From a862f4cafa69d2e65410c68658bc67d06a2241a8 Mon Sep 17 00:00:00 2001 From: imhson Date: Tue, 25 Apr 2023 15:31:52 +0700 Subject: [PATCH 01/69] init assets page --- src/assets/icons/search.svg | 3 + .../Tabs/{PureTab => NormalTab}/Tab/index.tsx | 0 .../{PureTab => NormalTab}/TabPanel/index.tsx | 0 .../Tabs/{PureTab => NormalTab}/index.tsx | 13 +---- src/pages/Assets/NFTs/index.tsx | 10 ++++ src/pages/Assets/Tokens/index.tsx | 56 +++++++++++++++++++ src/pages/Assets/index.tsx | 29 ++++++++++ src/pages/Transactions/index.tsx | 47 ++++++++-------- src/pages/Transactions/styled.tsx | 2 +- src/routes/safe/index.tsx | 3 +- src/theme/styledComponentsTheme.ts | 1 + 11 files changed, 125 insertions(+), 39 deletions(-) create mode 100644 src/assets/icons/search.svg rename src/components/Tabs/{PureTab => NormalTab}/Tab/index.tsx (100%) rename src/components/Tabs/{PureTab => NormalTab}/TabPanel/index.tsx (100%) rename src/components/Tabs/{PureTab => NormalTab}/index.tsx (56%) create mode 100644 src/pages/Assets/NFTs/index.tsx create mode 100644 src/pages/Assets/Tokens/index.tsx create mode 100644 src/pages/Assets/index.tsx diff --git a/src/assets/icons/search.svg b/src/assets/icons/search.svg new file mode 100644 index 0000000000..3f9bb81206 --- /dev/null +++ b/src/assets/icons/search.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/Tabs/PureTab/Tab/index.tsx b/src/components/Tabs/NormalTab/Tab/index.tsx similarity index 100% rename from src/components/Tabs/PureTab/Tab/index.tsx rename to src/components/Tabs/NormalTab/Tab/index.tsx diff --git a/src/components/Tabs/PureTab/TabPanel/index.tsx b/src/components/Tabs/NormalTab/TabPanel/index.tsx similarity index 100% rename from src/components/Tabs/PureTab/TabPanel/index.tsx rename to src/components/Tabs/NormalTab/TabPanel/index.tsx diff --git a/src/components/Tabs/PureTab/index.tsx b/src/components/Tabs/NormalTab/index.tsx similarity index 56% rename from src/components/Tabs/PureTab/index.tsx rename to src/components/Tabs/NormalTab/index.tsx index 71dde28b88..534b75265b 100644 --- a/src/components/Tabs/PureTab/index.tsx +++ b/src/components/Tabs/NormalTab/index.tsx @@ -1,21 +1,10 @@ import MuiTabs from '@material-ui/core/Tabs' import styled from 'styled-components' const Wrap = styled(MuiTabs)` - background: #363843; - border: 1px solid #494c58; - border-radius: 8px; - width: fit-content; - min-height: unset; - padding: 4px; - .MuiTabs-flexContainer { - z-index: 2; - position: relative; - } .MuiTabs-indicator { z-index: 1; - background-color: #24262e !important; + background-color: #fff !important; border-radius: 8px; - height: 100%; } ` export default function Tabs({ value, onChange, children }: any) { diff --git a/src/pages/Assets/NFTs/index.tsx b/src/pages/Assets/NFTs/index.tsx new file mode 100644 index 0000000000..5458711dc4 --- /dev/null +++ b/src/pages/Assets/NFTs/index.tsx @@ -0,0 +1,10 @@ +import { ReactElement, useState } from 'react' +import styled from 'styled-components' +const Wrap = styled.div`` + +function NFTs(props): ReactElement { + const [tab, setTab] = useState(0) + return +} + +export default NFTs diff --git a/src/pages/Assets/Tokens/index.tsx b/src/pages/Assets/Tokens/index.tsx new file mode 100644 index 0000000000..ff88d693ae --- /dev/null +++ b/src/pages/Assets/Tokens/index.tsx @@ -0,0 +1,56 @@ +import { ReactElement, useState } from 'react' +import styled from 'styled-components' +import SearchIcon from 'src/assets/icons/search.svg' +const Wrap = styled.div` + background: ${(props) => props.theme.backgroundPrimary}; + border-radius: 8px; + margin-top: 24px; + > .header { + padding: 16px 20px; + display: flex; + justify-content: space-between; + align-items: center; + > .title { + font-weight: 600; + font-size: 22px; + line-height: 28px; + } + .token-search-input { + border: 1px solid #494c58; + border-radius: 8px; + padding: 8px 16px; + gap: 8px; + display: flex; + align-items: center; + input { + font-family: inherit; + font-size: 12px; + line-height: 16px; + background: transparent; + border: none; + outline: none; + color: #fff; + min-width: 300px; + } + } + } +` + +function Tokens(props): ReactElement { + const [tab, setTab] = useState(0) + return ( + +
+
Token list
+
+
+ + +
+
+
+
+ ) +} + +export default Tokens diff --git a/src/pages/Assets/index.tsx b/src/pages/Assets/index.tsx new file mode 100644 index 0000000000..c782ac056c --- /dev/null +++ b/src/pages/Assets/index.tsx @@ -0,0 +1,29 @@ +import { ReactElement, useState } from 'react' +import Breadcrumb from 'src/components/Breadcrumb' +import Tabs from 'src/components/Tabs/NormalTab' +import Tab from 'src/components/Tabs/NormalTab/Tab' +import styled from 'styled-components' +import Tokens from './Tokens' +import NFTs from './NFTs' +const Wrap = styled.div`` + +function Assets(props): ReactElement { + const [tab, setTab] = useState(0) + return ( + <> + + { + setTab(v) + }} + > + + + + {tab == 0 ? : } + + ) +} + +export default Assets diff --git a/src/pages/Transactions/index.tsx b/src/pages/Transactions/index.tsx index 807cbcb6cc..cd7a6bf641 100644 --- a/src/pages/Transactions/index.tsx +++ b/src/pages/Transactions/index.tsx @@ -1,12 +1,11 @@ import { Item } from '@aura/safe-react-components/dist/navigation/Tab' import { ReactElement, useEffect, useState } from 'react' -import { Redirect, Route, Switch, useHistory, useRouteMatch } from 'react-router-dom' +import { Redirect, Route, Switch, useHistory } from 'react-router-dom' import Icon from 'src/assets/icons/ChartBar.svg' import Breadcrumb from 'src/components/Breadcrumb' -import Tabs from 'src/components/Tabs/FilterTab' -import Tab from 'src/components/Tabs/FilterTab/Tab' -import { extractPrefixedSafeAddress, generateSafeRoute, SAFE_ROUTES } from 'src/routes/routes' -import { SAFE_EVENTS, useAnalytics } from 'src/utils/googleAnalytics' +import Tabs from 'src/components/Tabs/NormalTab' +import Tab from 'src/components/Tabs/NormalTab/Tab' +import { SAFE_ROUTES, extractPrefixedSafeAddress, generateSafeRoute } from 'src/routes/routes' import HistoryTransactions from './History' import QueueTransactions from './Queue' import { ContentWrapper, Wrapper } from './styled' @@ -34,26 +33,24 @@ const Transactions = (): ReactElement => { return ( -
- - { - setTab(v) - switch (v) { - case 0: - onTabChange(SAFE_ROUTES.TRANSACTIONS_QUEUE) - break - case 1: - onTabChange(SAFE_ROUTES.TRANSACTIONS_HISTORY) - break - } - }} - > - - - -
+ + { + setTab(v) + switch (v) { + case 0: + onTabChange(SAFE_ROUTES.TRANSACTIONS_QUEUE) + break + case 1: + onTabChange(SAFE_ROUTES.TRANSACTIONS_HISTORY) + break + } + }} + > + + + } /> diff --git a/src/pages/Transactions/styled.tsx b/src/pages/Transactions/styled.tsx index de9aa8dfc9..b0ef85d6f4 100644 --- a/src/pages/Transactions/styled.tsx +++ b/src/pages/Transactions/styled.tsx @@ -671,7 +671,7 @@ export const ScrollableTransactionsContainer = styled.div` color: #ffffff; margin: 0px 0px 16px; } - .section-title:last-of-type { + .section-title:not(:first-of-type) { margin-top: 32px; } ` diff --git a/src/routes/safe/index.tsx b/src/routes/safe/index.tsx index d00c72c1a6..a0e230ec0a 100644 --- a/src/routes/safe/index.tsx +++ b/src/routes/safe/index.tsx @@ -13,6 +13,7 @@ import { wrapInSuspense } from 'src/utils/wrapInSuspense' import SafeLoadError from './components/SafeLoadError' import ContractInteraction from 'src/pages/SmartContract/ContractInteraction' import CustomTransaction from 'src/pages/Avanced/Custom Transaction' +import Assets from 'src/pages/Assets' export const BALANCES_TAB_BTN_TEST_ID = 'balances-tab-btn' export const SETTINGS_TAB_BTN_TEST_ID = 'settings-tab-btn' @@ -90,7 +91,7 @@ const Container = (): React.ReactElement => { wrapInSuspense(, null)} + render={() => wrapInSuspense(, null)} /> Date: Tue, 25 Apr 2023 16:50:25 +0700 Subject: [PATCH 02/69] update asset pages --- src/pages/Assets/Tokens/index.tsx | 63 ++++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 2 deletions(-) diff --git a/src/pages/Assets/Tokens/index.tsx b/src/pages/Assets/Tokens/index.tsx index ff88d693ae..2078466966 100644 --- a/src/pages/Assets/Tokens/index.tsx +++ b/src/pages/Assets/Tokens/index.tsx @@ -1,15 +1,27 @@ import { ReactElement, useState } from 'react' import styled from 'styled-components' import SearchIcon from 'src/assets/icons/search.svg' +import { FilledButton, OutlinedNeutralButton } from 'src/components/Button' +import DenseTable, { StyledTableCell, StyledTableRow } from 'src/components/Table/DenseTable' +import { useSelector } from 'react-redux' +import { extendedSafeTokensSelector } from 'src/utils/safeUtils/selector' +import { formatNativeCurrency } from 'src/utils' +import SendingPopup from 'src/components/Popup/SendingPopup' + const Wrap = styled.div` background: ${(props) => props.theme.backgroundPrimary}; border-radius: 8px; + overflow: hidden; margin-top: 24px; > .header { padding: 16px 20px; display: flex; justify-content: space-between; align-items: center; + > div:nth-child(2) { + display: flex; + gap: 32px; + } > .title { font-weight: 600; font-size: 22px; @@ -35,9 +47,23 @@ const Wrap = styled.div` } } ` - +const TokenInfo = styled.div` + display: flex; + align-items: center; + gap: 8px; + font-size: 14px; + line-height: 18px; + text-transform: uppercase; + img { + width: 24px; + height: 24px; + border-radius: 12px; + } +` function Tokens(props): ReactElement { - const [tab, setTab] = useState(0) + const [open, setOpen] = useState(false) + const [selectedToken, setSelectedToken] = useState(undefined) + const safeTokens: any = useSelector(extendedSafeTokensSelector) return (
@@ -47,8 +73,41 @@ function Tokens(props): ReactElement {
+ Manage token + + {safeTokens.map((token, index) => { + console.log(token) + return ( + + + + + {token.name || 'Unkonwn token'} + + + + {formatNativeCurrency(token.balance.tokenBalance)} + + { + setOpen(true) + setSelectedToken(token.address) + }} + > + Send + + + Receive + + + + ) + })} + + {}} onClose={() => setOpen(false)} />
) } From 2a5f2659a0dc20040d0f89d4ba39acc8437cf7c3 Mon Sep 17 00:00:00 2001 From: imhson Date: Tue, 25 Apr 2023 17:23:52 +0700 Subject: [PATCH 03/69] update asset pages --- src/assets/icons/ArrowUpRight.svg | 4 ++ src/components/Button/index.tsx | 65 ++++++++++++++++++++----------- src/pages/Assets/Tokens/index.tsx | 30 +++++++------- 3 files changed, 64 insertions(+), 35 deletions(-) create mode 100644 src/assets/icons/ArrowUpRight.svg diff --git a/src/assets/icons/ArrowUpRight.svg b/src/assets/icons/ArrowUpRight.svg new file mode 100644 index 0000000000..def90ab5e8 --- /dev/null +++ b/src/assets/icons/ArrowUpRight.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx index de94e95c73..3af45a59b0 100644 --- a/src/components/Button/index.tsx +++ b/src/components/Button/index.tsx @@ -77,10 +77,17 @@ export const OutlinedButtonWrap = styled.button<{ disabled?: boolean }>` } } ` - -export const OutlinedNeutralButton = styled.button<{ disabled?: boolean; color?: string }>` +export const OutlinedNeutralButton = ({ children, ...rest }) => { + return ( + +
{children}
+
+ ) +} +export const OutlinedNeutralButtonWrap = styled.button<{ disabled?: boolean; color?: string }>` background: transparent; cursor: pointer; + padding: 0 !important; border: 1px solid ${(props) => (props.color ? props.color : '#717582')}; border-radius: 8px; font-weight: 400; @@ -91,40 +98,54 @@ export const OutlinedNeutralButton = styled.button<{ disabled?: boolean; color?: letter-spacing: 0.01em; color: ${(props) => (props.color ? props.color : '#fff')}; white-space: nowrap; - padding: 10px 24px; - > i, - img, - svg, - .icon { - margin: 0px 8px 0px 0px; - } + &:disabled { cursor: not-allowed; pointer-events: unset; - background: #494c58; + color: #717582; + > div { + background: #131419; + } } + > div { + padding: 10px 24px; + border-radius: 8px; + display: flex; + > i, + img, + svg, + .icon { + margin: 0px 8px 0px 0px; + } + } + &:not(:disabled)&:hover { - background: #363843; + > div { + background: #363843; + } } &:active { - background: #24262e; - } - &.loading { - pointer-events: unset; - background: #24262e; + > div { + background: #24262e; + } } + &.small { font-size: 12px; line-height: 16px; - padding: 8px 16px; - > i, - img, - svg, - .icon { - margin: 0px 6px 0px 0px; + > div { + padding: 8px 16px; + border-radius: 8px; + > i, + img, + svg, + .icon { + margin: 0px 6px 0px 0px; + } } } ` + export const TextButton = styled(Button)` border: none; padding: 0 !important; diff --git a/src/pages/Assets/Tokens/index.tsx b/src/pages/Assets/Tokens/index.tsx index 2078466966..781c5df1d0 100644 --- a/src/pages/Assets/Tokens/index.tsx +++ b/src/pages/Assets/Tokens/index.tsx @@ -7,7 +7,7 @@ import { useSelector } from 'react-redux' import { extendedSafeTokensSelector } from 'src/utils/safeUtils/selector' import { formatNativeCurrency } from 'src/utils' import SendingPopup from 'src/components/Popup/SendingPopup' - +import sendIcon from 'src/assets/icons/ArrowUpRight.svg' const Wrap = styled.div` background: ${(props) => props.theme.backgroundPrimary}; border-radius: 8px; @@ -90,18 +90,22 @@ function Tokens(props): ReactElement { {formatNativeCurrency(token.balance.tokenBalance)} - { - setOpen(true) - setSelectedToken(token.address) - }} - > - Send - - - Receive - +
+ { + setOpen(true) + setSelectedToken(token.address) + }} + > + + Send + + + + Receive + +
) From 9ff0ed443235ce4b4f5138a1faf1ee5dc46bb6c1 Mon Sep 17 00:00:00 2001 From: imhson Date: Wed, 26 Apr 2023 15:06:29 +0700 Subject: [PATCH 04/69] change signer file --- src/logic/providers/signing.ts | 121 ------------------------------- src/utils/signer.ts | 129 ++++++++++++++++++++++++++++++++- src/utils/txTypes.ts | 100 +++++++++++++++++++++++++ 3 files changed, 226 insertions(+), 124 deletions(-) delete mode 100644 src/logic/providers/signing.ts create mode 100644 src/utils/txTypes.ts diff --git a/src/logic/providers/signing.ts b/src/logic/providers/signing.ts deleted file mode 100644 index 79087ae655..0000000000 --- a/src/logic/providers/signing.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { encodeSecp256k1Pubkey, makeSignDoc as makeSignDocAmino } from '@cosmjs/amino' -import { createWasmAminoConverters } from '@cosmjs/cosmwasm-stargate' -import { fromBase64 } from '@cosmjs/encoding' -import { Int53 } from '@cosmjs/math' -import { Registry, TxBodyEncodeObject, encodePubkey, makeAuthInfoBytes } from '@cosmjs/proto-signing' -import { - AminoTypes, - SequenceResponse, - SignerData, - StdFee, - createAuthzAminoConverters, - createBankAminoConverters, - createDistributionAminoConverters, - createFreegrantAminoConverters, - createGovAminoConverters, - createIbcAminoConverters, - createStakingAminoConverters, -} from '@cosmjs/stargate' -import { KeplrIntereactionOptions } from '@keplr-wallet/types' -import { SignMode } from 'cosmjs-types/cosmos/tx/signing/v1beta1/signing' -import { TxRaw } from 'cosmjs-types/cosmos/tx/v1beta1/tx' -import { getChainInfo, getInternalChainId } from 'src/config' -import { getProvider } from 'src/logic/providers/utils/wallets' -import { WALLETS_NAME } from 'src/logic/wallets/constant/wallets' -import { loadLastUsedProvider } from 'src/logic/wallets/store/middlewares/providerWatcher' -import { getAccountOnChain } from 'src/services' -import { TxTypes } from './txTypes' -const getDefaultOptions = (): KeplrIntereactionOptions => ({ - sign: { - preferNoSetMemo: true, - preferNoSetFee: true, - disableBalanceCheck: true, - }, -}) - -export const signMessage = async ( - chainId: string, - safeAddress: string, - messages: any, - fee: StdFee, - sequence?: string | undefined, - memo?: string, -): Promise => { - try { - const loadLastUsedProviderResult = await loadLastUsedProvider() - const provider = loadLastUsedProviderResult - ? await getProvider(loadLastUsedProviderResult as WALLETS_NAME) - : undefined - if (!provider) - throw new Error(`An error occurred while loading wallet provider. Please disconnect your wallet and try again.`) - provider.defaultOptions = getDefaultOptions() - const offlineSignerOnlyAmino = (window as any).getOfflineSignerOnlyAmino - if (offlineSignerOnlyAmino) { - const signer = await offlineSignerOnlyAmino(chainId) - if (!signer) - throw new Error(`An error occurred while loading signer. Please disconnect your wallet and try again.`) - const account = await signer.getAccounts() - const onlineData: SequenceResponse = (await getAccountOnChain(safeAddress, getInternalChainId())).Data - const signerData: SignerData = { - accountNumber: onlineData.accountNumber, - sequence: sequence ? +sequence : onlineData?.sequence, - chainId, - } - const signerAddress = account[0].address - if (!(signerAddress && messages && fee && signerData)) { - throw new Error(`An error occurred while loading signing payload. Please disconnect your wallet and try again.`) - } - ;(window as any).signObject = { signerAddress, messages, fee, memo, signerData } - const registry = new Registry(TxTypes) - const aminoTypes = new AminoTypes({ - ...createBankAminoConverters(), - ...createStakingAminoConverters(getChainInfo().shortName), - ...createDistributionAminoConverters(), - ...createGovAminoConverters(), - ...createWasmAminoConverters(), - ...createAuthzAminoConverters(), - ...createFreegrantAminoConverters(), - ...createIbcAminoConverters(), - }) - const pubkey = encodePubkey(encodeSecp256k1Pubkey(account[0].pubkey)) - const signMode = SignMode.SIGN_MODE_LEGACY_AMINO_JSON - const msgs = messages.map((msg) => { - return aminoTypes.toAmino(msg) - }) - const signDoc = makeSignDocAmino(msgs, fee, chainId, memo, signerData.accountNumber, signerData.sequence) - const { signature, signed } = await signer.signAmino(signerAddress, signDoc) - const signedTxBody = { - messages: signed.msgs.map((msg) => aminoTypes.fromAmino(msg)), - memo: signed.memo, - } - const signedTxBodyEncodeObject: TxBodyEncodeObject = { - typeUrl: '/cosmos.tx.v1beta1.TxBody', - value: signedTxBody, - } - const signedTxBodyBytes = registry.encode(signedTxBodyEncodeObject) - const signedGasLimit = Int53.fromString(signed.fee.gas).toNumber() - const signedSequence = Int53.fromString(signed.sequence).toNumber() - const signedAuthInfoBytes = makeAuthInfoBytes( - [{ pubkey, sequence: signedSequence }], - signed.fee.amount, - signedGasLimit, - signMode, - ) - const respone = TxRaw.fromPartial({ - bodyBytes: signedTxBodyBytes, - authInfoBytes: signedAuthInfoBytes, - signatures: [fromBase64(signature.signature)], - }) - return { - ...respone, - accountNumber: onlineData.accountNumber, - sequence: sequence ? +sequence : onlineData?.sequence, - signerAddress, - } - } - return undefined - } catch (error) { - throw new Error(error) - } -} - diff --git a/src/utils/signer.ts b/src/utils/signer.ts index acb97e19a6..039f5ce2f8 100644 --- a/src/utils/signer.ts +++ b/src/utils/signer.ts @@ -1,12 +1,34 @@ import { Dispatch } from 'redux' import { getChainInfo, getInternalChainId } from 'src/config' -import { toBase64, toUtf8 } from '@cosmjs/encoding' +import { encodeSecp256k1Pubkey, makeSignDoc as makeSignDocAmino } from '@cosmjs/amino' +import { createWasmAminoConverters } from '@cosmjs/cosmwasm-stargate' +import { fromBase64, toBase64, toUtf8 } from '@cosmjs/encoding' +import { Int53 } from '@cosmjs/math' +import { Registry, TxBodyEncodeObject, encodePubkey, makeAuthInfoBytes } from '@cosmjs/proto-signing' +import { + AminoTypes, + SequenceResponse, + SignerData, + StdFee, + createAuthzAminoConverters, + createBankAminoConverters, + createDistributionAminoConverters, + createFreegrantAminoConverters, + createGovAminoConverters, + createIbcAminoConverters, + createStakingAminoConverters, +} from '@cosmjs/stargate' +import { KeplrIntereactionOptions } from '@keplr-wallet/types' +import { SignMode } from 'cosmjs-types/cosmos/tx/signing/v1beta1/signing' +import { TxRaw } from 'cosmjs-types/cosmos/tx/v1beta1/tx' import { generatePath } from 'react-router-dom' import { ERROR, NOTIFICATIONS, enhanceSnackbarForAction } from 'src/logic/notifications' import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar' -import { signMessage } from 'src/logic/providers/signing' +import { getProvider } from 'src/logic/providers/utils/wallets' import fetchTransactions from 'src/logic/safe/store/actions/transactions/fetchTransactions' +import { WALLETS_NAME } from 'src/logic/wallets/constant/wallets' +import { loadLastUsedProvider } from 'src/logic/wallets/store/middlewares/providerWatcher' import { SAFE_ADDRESS_SLUG, SAFE_ROUTES, @@ -15,10 +37,16 @@ import { getPrefixedSafeAddressSlug, history, } from 'src/routes/routes' -import { changeTransactionSequenceById, confirmSafeTransaction, createSafeTransaction } from 'src/services' +import { + changeTransactionSequenceById, + confirmSafeTransaction, + createSafeTransaction, + getAccountOnChain, +} from 'src/services' import { MESSAGES_CODE } from 'src/services/constant/message' import { ICreateSafeTransaction } from 'src/types/transaction' import { calcFee } from 'src/utils' +import { TxTypes } from './txTypes' export const signAndCreateTransaction = ( message: any[], @@ -289,3 +317,98 @@ export const signAndChangeTransactionSequence = ) } } + +const getDefaultOptions = (): KeplrIntereactionOptions => ({ + sign: { + preferNoSetMemo: true, + preferNoSetFee: true, + disableBalanceCheck: true, + }, +}) + +export const signMessage = async ( + chainId: string, + safeAddress: string, + messages: any, + fee: StdFee, + sequence?: string | undefined, + memo?: string, +): Promise => { + try { + const loadLastUsedProviderResult = await loadLastUsedProvider() + const provider = loadLastUsedProviderResult + ? await getProvider(loadLastUsedProviderResult as WALLETS_NAME) + : undefined + if (!provider) + throw new Error(`An error occurred while loading wallet provider. Please disconnect your wallet and try again.`) + provider.defaultOptions = getDefaultOptions() + const offlineSignerOnlyAmino = (window as any).getOfflineSignerOnlyAmino + if (offlineSignerOnlyAmino) { + const signer = await offlineSignerOnlyAmino(chainId) + if (!signer) + throw new Error(`An error occurred while loading signer. Please disconnect your wallet and try again.`) + const account = await signer.getAccounts() + const onlineData: SequenceResponse = (await getAccountOnChain(safeAddress, getInternalChainId())).Data + const signerData: SignerData = { + accountNumber: onlineData.accountNumber, + sequence: sequence ? +sequence : onlineData?.sequence, + chainId, + } + const signerAddress = account[0].address + if (!(signerAddress && messages && fee && signerData)) { + throw new Error(`An error occurred while loading signing payload. Please disconnect your wallet and try again.`) + } + ;(window as any).signObject = { signerAddress, messages, fee, memo, signerData } + const registry = new Registry(TxTypes) + const aminoTypes = new AminoTypes({ + ...createBankAminoConverters(), + ...createStakingAminoConverters(getChainInfo().shortName), + ...createDistributionAminoConverters(), + ...createGovAminoConverters(), + ...createWasmAminoConverters(), + ...createAuthzAminoConverters(), + ...createFreegrantAminoConverters(), + ...createIbcAminoConverters(), + }) + const pubkey = encodePubkey(encodeSecp256k1Pubkey(account[0].pubkey)) + const signMode = SignMode.SIGN_MODE_LEGACY_AMINO_JSON + const msgs = messages.map((msg) => { + return aminoTypes.toAmino(msg) + }) + const signDoc = makeSignDocAmino(msgs, fee, chainId, memo, signerData.accountNumber, signerData.sequence) + const { signature, signed } = await signer.signAmino(signerAddress, signDoc) + const signedTxBody = { + messages: signed.msgs.map((msg) => aminoTypes.fromAmino(msg)), + memo: signed.memo, + } + const signedTxBodyEncodeObject: TxBodyEncodeObject = { + typeUrl: '/cosmos.tx.v1beta1.TxBody', + value: signedTxBody, + } + const signedTxBodyBytes = registry.encode(signedTxBodyEncodeObject) + const signedGasLimit = Int53.fromString(signed.fee.gas).toNumber() + const signedSequence = Int53.fromString(signed.sequence).toNumber() + const signedAuthInfoBytes = makeAuthInfoBytes( + [{ pubkey, sequence: signedSequence }], + signed.fee.amount, + signedGasLimit, + signMode, + ) + const respone = TxRaw.fromPartial({ + bodyBytes: signedTxBodyBytes, + authInfoBytes: signedAuthInfoBytes, + signatures: [fromBase64(signature.signature)], + }) + return { + ...respone, + accountNumber: onlineData.accountNumber, + sequence: sequence ? +sequence : onlineData?.sequence, + signerAddress, + } + } + return undefined + } catch (error) { + throw new Error(error) + } +} + diff --git a/src/utils/txTypes.ts b/src/utils/txTypes.ts new file mode 100644 index 0000000000..08b0e01de5 --- /dev/null +++ b/src/utils/txTypes.ts @@ -0,0 +1,100 @@ +import { GeneratedType } from '@cosmjs/proto-signing' +import { MsgSend, MsgMultiSend } from 'cosmjs-types/cosmos/bank/v1beta1/tx' +import { Coin } from 'cosmjs-types/cosmos/base/v1beta1/coin' +import { + MsgFundCommunityPool, + MsgSetWithdrawAddress, + MsgWithdrawDelegatorReward, + MsgWithdrawValidatorCommission, +} from 'cosmjs-types/cosmos/distribution/v1beta1/tx' +import { MsgVote, MsgDeposit, MsgSubmitProposal } from 'cosmjs-types/cosmos/gov/v1beta1/tx' +import { + MsgClearAdmin, + MsgExecuteContract, + MsgMigrateContract, + MsgStoreCode, + MsgInstantiateContract, + MsgUpdateAdmin, +} from 'cosmjs-types/cosmwasm/wasm/v1/tx' +import { + MsgBeginRedelegate, + MsgDelegate, + MsgUndelegate, + MsgCreateValidator, + MsgEditValidator, +} from 'cosmjs-types/cosmos/staking/v1beta1/tx' +import { MsgExec, MsgGrant, MsgRevoke } from 'cosmjs-types/cosmos/authz/v1beta1/tx' +import { MsgGrantAllowance, MsgRevokeAllowance } from 'cosmjs-types/cosmos/feegrant/v1beta1/tx' +import { MsgTransfer } from 'cosmjs-types/ibc/applications/transfer/v1/tx' +import { + MsgAcknowledgement, + MsgChannelCloseConfirm, + MsgChannelCloseInit, + MsgChannelOpenAck, + MsgChannelOpenConfirm, + MsgChannelOpenInit, + MsgChannelOpenTry, + MsgRecvPacket, + MsgTimeout, + MsgTimeoutOnClose, +} from 'cosmjs-types/ibc/core/channel/v1/tx' +import { + MsgCreateClient, + MsgSubmitMisbehaviour, + MsgUpdateClient, + MsgUpgradeClient, +} from 'cosmjs-types/ibc/core/client/v1/tx' +import { + MsgConnectionOpenAck, + MsgConnectionOpenConfirm, + MsgConnectionOpenInit, + MsgConnectionOpenTry, +} from 'cosmjs-types/ibc/core/connection/v1/tx' + +export const TxTypes: Iterable<[string, GeneratedType]> = [ + ['/cosmos.base.v1beta1.Coin', Coin], + ['/cosmos.bank.v1beta1.MsgSend', MsgSend], + ['/cosmos.bank.v1beta1.MsgMultiSend', MsgMultiSend], + ['/cosmos.staking.v1beta1.MsgDelegate', MsgDelegate], + ['/cosmos.staking.v1beta1.MsgBeginRedelegate', MsgBeginRedelegate], + ['/cosmos.staking.v1beta1.MsgUndelegate', MsgUndelegate], + ['/cosmos.staking.v1beta1.MsgCreateValidator', MsgCreateValidator], + ['/cosmos.staking.v1beta1.MsgEditValidator', MsgEditValidator], + ['/cosmos.gov.v1beta1.MsgVote', MsgVote], + ['/cosmos.gov.v1beta1.MsgDeposit', MsgDeposit], + ['/cosmos.gov.v1beta1.MsgSubmitProposal', MsgSubmitProposal], + ['/cosmos.authz.v1beta1.MsgExec', MsgExec], + ['/cosmos.authz.v1beta1.MsgGrant', MsgGrant], + ['/cosmos.authz.v1beta1.MsgRev', MsgRevoke], + ['/cosmos.distribution.v1beta1.MsgFundCommunityPool', MsgFundCommunityPool], + ['/cosmos.distribution.v1beta1.MsgSetWithdrawAddress', MsgSetWithdrawAddress], + ['/cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward', MsgWithdrawDelegatorReward], + ['/cosmos.distribution.v1beta1.MsgWithdrawValidatorCommission', MsgWithdrawValidatorCommission], + ['/cosmos.feegrant.v1beta1.MsgGrantAllowance', MsgGrantAllowance], + ['/cosmos.feegrant.v1beta1.MsgRevokeAllowance', MsgRevokeAllowance], + ['/ibc.applications.transfer.v1.MsgTransfer', MsgTransfer], + ['/ibc.core.channel.v1.MsgAcknowledgement', MsgAcknowledgement], + ['/ibc.core.channel.v1.MsgChannelCloseConfirm', MsgChannelCloseConfirm], + ['/ibc.core.channel.v1.MsgChannelCloseInit', MsgChannelCloseInit], + ['/ibc.core.channel.v1.MsgChannelOpenAck', MsgChannelOpenAck], + ['/ibc.core.channel.v1.MsgChannelOpenConfirm', MsgChannelOpenConfirm], + ['/ibc.core.channel.v1.MsgChannelOpenInit', MsgChannelOpenInit], + ['/ibc.core.channel.v1.MsgChannelOpenTry', MsgChannelOpenTry], + ['/ibc.core.channel.v1.MsgRecvPacket', MsgRecvPacket], + ['/ibc.core.channel.v1.MsgTimeout', MsgTimeout], + ['/ibc.core.channel.v1.MsgTimeoutOnClose', MsgTimeoutOnClose], + ['/ibc.core.client.v1.MsgCreateClient', MsgCreateClient], + ['/ibc.core.client.v1.MsgSubmitMisbehaviour', MsgSubmitMisbehaviour], + ['/ibc.core.client.v1.MsgUpdateClient', MsgUpdateClient], + ['/ibc.core.client.v1.MsgUpgradeClient', MsgUpgradeClient], + ['/ibc.core.connection.v1.MsgConnectionOpenAck', MsgConnectionOpenAck], + ['/ibc.core.connection.v1.MsgConnectionOpenConfirm', MsgConnectionOpenConfirm], + ['/ibc.core.connection.v1.MsgConnectionOpenInit', MsgConnectionOpenInit], + ['/ibc.core.connection.v1.MsgConnectionOpenTry', MsgConnectionOpenTry], + ['/cosmwasm.wasm.v1.MsgClearAdmin', MsgClearAdmin], + ['/cosmwasm.wasm.v1.MsgExecuteContract', MsgExecuteContract], + ['/cosmwasm.wasm.v1.MsgMigrateContract', MsgMigrateContract], + ['/cosmwasm.wasm.v1.MsgStoreCode', MsgStoreCode], + ['/cosmwasm.wasm.v1.MsgInstantiateContract', MsgInstantiateContract], + ['/cosmwasm.wasm.v1.MsgUpdateAdmin', MsgUpdateAdmin], +] From ef968ba3955228e26ecd421af35eb24b37a3f363 Mon Sep 17 00:00:00 2001 From: imhson Date: Wed, 26 Apr 2023 16:22:11 +0700 Subject: [PATCH 05/69] remove unused import --- src/App/index.tsx | 46 +-- src/components/AddressInfo/index.tsx | 2 +- src/components/Button/index.tsx | 4 +- src/components/CardStaking/index.tsx | 134 -------- src/components/CardStaking/style.ts | 93 ------ src/components/CardVoting/index.tsx | 204 ------------ src/components/ConnectWalletModal/styles.tsx | 2 +- .../CustomTransactionMessage/BigMsg.tsx | 7 +- .../CustomTransactionMessage/SmallMsg.tsx | 6 +- src/components/DetailVoting/index.tsx | 49 --- src/components/Divider/divider.stories.tsx | 25 -- src/components/InfiniteScroll/index.tsx | 4 +- src/components/Input/Address/style.tsx | 3 - src/components/Input/AmountInput/index.tsx | 2 +- src/components/JsonschemaForm/utils.tsx | 2 +- src/components/List/ListItemText/index.tsx | 27 -- src/components/Modal/index.stories.tsx | 174 ---------- src/components/Modal/type.tsx | 7 +- src/components/ModalNew/index.tsx | 33 -- .../Popup/MultiSendPopup/styles.tsx | 36 -- src/components/Popup/SendingPopup/index.tsx | 9 - src/components/Popup/SendingPopup/styles.tsx | 37 +-- src/components/Stepper/Stepper.tsx | 3 +- src/components/StepperForm/StepperForm.tsx | 4 +- src/components/SuspenseContainer/index.tsx | 35 -- src/components/Table/sorting.ts | 4 +- src/components/TableVoting/index.tsx | 79 ----- .../Tabs/NormalTab/TabPanel/index.tsx | 22 -- src/components/TextBox/index.tsx | 20 -- src/components/VotingBarDetail/index.tsx | 127 ------- src/components/WalletSwitch/styles.tsx | 17 - src/components/forms/AddressInput/index.tsx | 2 +- src/components/forms/validator.ts | 7 +- src/components/layout/Backdrop/index.tsx | 23 -- src/components/layout/Pre/index.tsx | 14 - src/components/layout/Table/index.tsx | 27 +- .../Header/components/Layout/styles.tsx | 7 +- src/layout/Sidebar/DebugToggle/index.tsx | 30 -- .../Sidebar/SafeHeader/index.stories.tsx | 18 - src/layout/Sidebar/SafeHeader/index.tsx | 2 +- src/layout/Sidebar/SafeHeader/styles.tsx | 28 +- src/logic/addressBook/store/reducer/index.ts | 2 +- .../addressBook/store/selectors/index.ts | 9 +- src/logic/addressBook/utils/index.ts | 11 - .../appearance/actions/setCopyShortName.ts | 6 - .../appearance/actions/setShowShortName.ts | 6 - src/logic/appearance/selectors/index.ts | 6 - src/logic/checkTerm/store/actions/setTerm.ts | 3 +- src/logic/checkTerm/store/reducer/term.ts | 2 +- .../collectibles/sources/collectibles.d.ts | 8 +- .../collectibles/store/selectors/index.ts | 4 +- src/logic/collectibles/utils/index.ts | 4 +- src/logic/config/store/selectors/index.ts | 2 +- .../sources/ABIService/index.ts | 19 +- src/logic/contracts/api/masterCopies.ts | 38 --- src/logic/contracts/methodIds.ts | 232 ------------- .../api/fetchAvailableCurrencies.ts | 6 - .../store/actions/setAvailableCurrencies.ts | 5 - .../actions/updateAvailableCurrencies.ts | 19 -- .../currencyValues/store/selectors/index.ts | 4 +- .../store/reducer/currentSession.ts | 2 +- src/logic/delegation/store/actions/index.ts | 2 +- src/logic/delegation/store/reducer/index.ts | 2 +- src/logic/hooks/useEstimateTransactionGas.tsx | 20 +- src/logic/keplr/keplr.ts | 2 +- src/logic/keplr/useKeplrKeyStoreChange.ts | 2 +- .../store/actions/addOrUpdateProposals.ts | 7 - src/logic/proposal/store/selectors/index.ts | 36 -- src/logic/providers/constants/constant.ts | 2 +- .../providers/hooks/useKeplrKeyStoreChange.ts | 32 -- src/logic/providers/txTypes.ts | 100 ------ src/logic/providers/utils/index.ts | 4 +- src/logic/providers/utils/message.ts | 9 - src/logic/safe/api/fetchSafesByOwner.ts | 9 - .../fetchLatestMasterContractVersion.ts | 13 - .../store/actions/fetchTransactionDetails.ts | 30 +- .../safe/store/actions/processTransaction.ts | 203 ------------ .../safe/store/actions/setLastOpenedSafe.ts | 7 - .../loadGatewayTransactions.ts | 77 +---- .../loadOutgoingTransactions.ts | 41 --- src/logic/safe/store/actions/types.d.ts | 2 +- src/logic/safe/store/index.ts | 46 ++- src/logic/safe/store/models/confirmation.ts | 9 - src/logic/safe/store/models/safe.ts | 2 +- .../safe/store/models/types/gateway.d.ts | 2 +- .../safe/store/models/types/transaction.ts | 6 - .../safe/store/models/types/transactions.d.ts | 35 +- .../safe/store/reducer/gatewayTransactions.ts | 2 +- .../safe/store/reducer/localTransactions.ts | 2 +- .../store/selectors/gatewayTransactions.ts | 45 +-- src/logic/safe/store/selectors/index.ts | 18 +- src/logic/safe/store/selectors/utils.ts | 7 +- src/logic/safe/transactions/gas.ts | 2 +- src/logic/safe/utils/guardManager.ts | 2 +- src/logic/tokens/store/model/token.ts | 2 +- src/logic/tokens/store/selectors/index.ts | 10 +- src/logic/validator/store/actions/index.ts | 2 +- src/logic/validator/store/reducer/index.ts | 2 +- src/logic/wallets/ethAddresses.ts | 6 - src/logic/wallets/onboard.ts | 2 +- src/logic/wallets/utils/network.ts | 14 +- .../CreateEditEntryModal/index.tsx | 6 +- .../AddressBook/DeleteEntryModal/index.tsx | 2 +- .../EllipsisTransactionDetails/index.tsx | 93 ------ src/pages/AddressBook/columns.ts | 6 +- src/pages/AddressBook/style.ts | 13 +- src/pages/Staking/constant.tsx | 7 - src/pages/Transactions/styled.tsx | 236 +------------ src/pages/Voting/ReviewTxPopup.tsx | 12 +- .../CancelSafePage/fields/_loadFields.tsx | 22 -- .../fields/cancelSafeFields.tsx | 2 - src/routes/CancelSafePage/fields/utils.ts | 26 -- .../CancelSafePage/steps/ReviewAllowStep.tsx | 1 - .../components/SafeCreationProcess.tsx | 312 ------------------ .../fields/createSafeFields.tsx | 2 +- .../steps/NameNewSafeStep/styles.tsx | 5 +- .../styles.tsx | 22 +- src/routes/CreateSafePage/styles.tsx | 19 +- src/routes/LoadSafePage/fields/loadFields.tsx | 2 +- .../steps/LoadSafeAddressStep/styles.tsx | 7 +- src/routes/routes.ts | 14 +- .../Apps/components/AppCard/index.stories.tsx | 27 -- .../components/Balances/SendModal/index.tsx | 2 +- .../screens/AddressBookInput/index.tsx | 4 +- .../ContractInteraction/Buttons/index.tsx | 2 +- .../ContractInteraction/ContractABI/index.tsx | 2 +- .../EthAddressInput/index.tsx | 2 +- .../ContractInteraction/utils/index.ts | 3 +- .../safe/components/Balances/dataFetcher.ts | 2 +- .../safe/components/Balances/utils/index.ts | 1 - src/routes/safe/components/SafeLoadError.tsx | 2 +- .../Settings/Advanced/TransactionGuard.tsx | 2 +- .../Settings/Advanced/dataFetcher.ts | 2 +- .../components/Settings/Appearance/index.tsx | 63 ---- .../components/Settings/Appearance/styles.tsx | 15 - .../ManageOwners/AddOwnerModal/index.tsx | 2 +- .../AddOwnerModal/screens/OwnerForm/index.tsx | 6 +- .../AddOwnerModal/screens/Review/index.tsx | 2 +- .../screens/ThresholdForm/index.tsx | 2 +- .../ManageOwners/EditOwnerModal/index.tsx | 4 +- .../ManageOwners/RemoveOwnerModal/index.tsx | 2 +- .../screens/CheckOwner/index.tsx | 2 +- .../RemoveOwnerModal/screens/Review/index.tsx | 2 +- .../screens/ThresholdForm/index.tsx | 2 +- .../ManageOwners/ReplaceOwnerModal/index.tsx | 2 +- .../screens/OwnerForm/index.tsx | 6 +- .../screens/Review/index.tsx | 2 +- .../Settings/ManageOwners/dataFetcher.ts | 4 +- .../SpendingLimit/InfoDisplay/DataDisplay.tsx | 20 -- .../SpendingLimit/InfoDisplay/index.ts | 1 - .../SpendingLimit/LimitsTable/dataFetcher.ts | 4 +- .../SpendingLimit/LimitsTable/index.tsx | 92 ------ .../SpendingLimit/NewLimitModal/index.tsx | 30 +- .../Settings/SpendingLimit/NewLimitSteps.tsx | 77 ----- .../Settings/SpendingLimit/index.tsx | 72 ---- .../ChangeThreshold/index.tsx | 215 ------------ .../ChangeThreshold/style.ts | 17 - .../Settings/assets/icons/OwnersIcon.tsx | 11 - .../icons/RequiredConfirmationsIcon.tsx | 11 - .../Settings/assets/icons/SafeDetailsIcon.tsx | 14 - .../components/assets/AddressBookIcon.tsx | 12 - .../safe/components/assets/BalancesIcon.tsx | 16 - .../safe/components/assets/SettingsIcon.tsx | 11 - .../components/assets/TransactionsIcon.tsx | 11 - .../safe/container/hooks/useTransactionFee.ts | 141 -------- src/services/constant/common.ts | 2 +- src/services/index.ts | 15 +- src/test/utils/safeHelper.ts | 108 ------ src/types/proposal.ts | 4 +- src/utils/calc.ts | 59 +--- src/utils/clipboard.ts | 15 - src/utils/css.ts | 2 +- src/utils/date.ts | 2 +- src/utils/hooks/useKnownAddress.ts | 33 -- .../hooks/usePagedHistoryTransactions.ts | 2 +- src/utils/hooks/usePagedQueuedTransactions.ts | 2 +- src/utils/hooks/useTransactionDetails.ts | 2 +- src/utils/hooks/useTransactionStatus.ts | 2 +- src/utils/index.ts | 24 +- src/utils/intercom.ts | 26 -- src/utils/signer.ts | 2 +- src/utils/transactionUtils.ts | 254 +------------- 182 files changed, 227 insertions(+), 4637 deletions(-) delete mode 100644 src/components/CardStaking/index.tsx delete mode 100644 src/components/CardStaking/style.ts delete mode 100644 src/components/CardVoting/index.tsx delete mode 100644 src/components/DetailVoting/index.tsx delete mode 100644 src/components/Divider/divider.stories.tsx delete mode 100644 src/components/Input/Address/style.tsx delete mode 100644 src/components/List/ListItemText/index.tsx delete mode 100644 src/components/Modal/index.stories.tsx delete mode 100644 src/components/ModalNew/index.tsx delete mode 100644 src/components/SuspenseContainer/index.tsx delete mode 100644 src/components/TableVoting/index.tsx delete mode 100644 src/components/Tabs/NormalTab/TabPanel/index.tsx delete mode 100644 src/components/TextBox/index.tsx delete mode 100644 src/components/VotingBarDetail/index.tsx delete mode 100644 src/components/WalletSwitch/styles.tsx delete mode 100644 src/components/layout/Backdrop/index.tsx delete mode 100644 src/components/layout/Pre/index.tsx delete mode 100644 src/layout/Sidebar/DebugToggle/index.tsx delete mode 100644 src/layout/Sidebar/SafeHeader/index.stories.tsx delete mode 100644 src/logic/appearance/selectors/index.ts delete mode 100644 src/logic/contracts/api/masterCopies.ts delete mode 100644 src/logic/contracts/methodIds.ts delete mode 100644 src/logic/currencyValues/api/fetchAvailableCurrencies.ts delete mode 100644 src/logic/currencyValues/store/actions/updateAvailableCurrencies.ts delete mode 100644 src/logic/proposal/store/selectors/index.ts delete mode 100644 src/logic/providers/hooks/useKeplrKeyStoreChange.ts delete mode 100644 src/logic/providers/txTypes.ts delete mode 100644 src/logic/providers/utils/message.ts delete mode 100644 src/logic/safe/api/fetchSafesByOwner.ts delete mode 100644 src/logic/safe/store/actions/fetchLatestMasterContractVersion.ts delete mode 100644 src/logic/safe/store/actions/processTransaction.ts delete mode 100644 src/logic/safe/store/actions/setLastOpenedSafe.ts delete mode 100644 src/logic/safe/store/actions/transactions/fetchTransactions/loadOutgoingTransactions.ts delete mode 100644 src/logic/safe/store/models/confirmation.ts delete mode 100644 src/pages/AddressBook/EllipsisTransactionDetails/index.tsx delete mode 100644 src/pages/Staking/constant.tsx delete mode 100644 src/routes/CancelSafePage/fields/_loadFields.tsx delete mode 100644 src/routes/CancelSafePage/fields/utils.ts delete mode 100644 src/routes/CreateSafePage/components/SafeCreationProcess.tsx delete mode 100644 src/routes/safe/components/Apps/components/AppCard/index.stories.tsx delete mode 100644 src/routes/safe/components/Settings/Appearance/index.tsx delete mode 100644 src/routes/safe/components/Settings/Appearance/styles.tsx delete mode 100644 src/routes/safe/components/Settings/SpendingLimit/InfoDisplay/DataDisplay.tsx delete mode 100644 src/routes/safe/components/Settings/SpendingLimit/LimitsTable/index.tsx delete mode 100644 src/routes/safe/components/Settings/SpendingLimit/NewLimitSteps.tsx delete mode 100644 src/routes/safe/components/Settings/SpendingLimit/index.tsx delete mode 100644 src/routes/safe/components/Settings/ThresholdSettings/ChangeThreshold/index.tsx delete mode 100644 src/routes/safe/components/Settings/ThresholdSettings/ChangeThreshold/style.ts delete mode 100644 src/routes/safe/components/Settings/assets/icons/OwnersIcon.tsx delete mode 100644 src/routes/safe/components/Settings/assets/icons/RequiredConfirmationsIcon.tsx delete mode 100644 src/routes/safe/components/Settings/assets/icons/SafeDetailsIcon.tsx delete mode 100644 src/routes/safe/components/assets/AddressBookIcon.tsx delete mode 100644 src/routes/safe/components/assets/BalancesIcon.tsx delete mode 100644 src/routes/safe/components/assets/SettingsIcon.tsx delete mode 100644 src/routes/safe/components/assets/TransactionsIcon.tsx delete mode 100644 src/routes/safe/container/hooks/useTransactionFee.ts delete mode 100644 src/test/utils/safeHelper.ts delete mode 100644 src/utils/clipboard.ts delete mode 100644 src/utils/hooks/useKnownAddress.ts diff --git a/src/App/index.tsx b/src/App/index.tsx index ffeb3267aa..1d968cb743 100644 --- a/src/App/index.tsx +++ b/src/App/index.tsx @@ -1,6 +1,6 @@ -import { useContext, useEffect, useState } from 'react' import { makeStyles } from '@material-ui/core/styles' import { SnackbarProvider } from 'notistack' +import { useContext, useEffect } from 'react' import { useDispatch, useSelector } from 'react-redux' import styled from 'styled-components' @@ -8,34 +8,31 @@ import AlertIcon from 'src/assets/icons/alert.svg' import CheckIcon from 'src/assets/icons/check.svg' import ErrorIcon from 'src/assets/icons/error.svg' import InfoIcon from 'src/assets/icons/info.svg' -import AppLayout from 'src/layout' -import { SafeListSidebar, SafeListSidebarContext } from 'src/components/SafeListSidebar' import CookiesBanner from 'src/components/CookiesBanner' +import Modal from 'src/components/Modal' import Notifier from 'src/components/Notifier' +import { SafeListSidebar, SafeListSidebarContext } from 'src/components/SafeListSidebar' import Img from 'src/components/layout/Img' -import { currentSafeWithNames } from 'src/logic/safe/store/selectors' +import AppLayout from 'src/layout' import { currentCurrencySelector } from 'src/logic/currencyValues/store/selectors' -import Modal from 'src/components/Modal' -import SendModal from 'src/routes/safe/components/Balances/SendModal' -import { useLoadSafe } from 'src/logic/safe/hooks/useLoadSafe' import useConnectWallet from 'src/logic/hooks/useConnectWallet' -import { useSafeScheduledUpdates } from 'src/logic/safe/hooks/useSafeScheduledUpdates' +import { useLoadSafe } from 'src/logic/safe/hooks/useLoadSafe' import useSafeActions from 'src/logic/safe/hooks/useSafeActions' +import { useSafeScheduledUpdates } from 'src/logic/safe/hooks/useSafeScheduledUpdates' +import { currentSafeWithNames } from 'src/logic/safe/store/selectors' import { formatAmountInUsFormat } from 'src/logic/tokens/utils/formatAmount' import { grantedSelector } from 'src/utils/safeUtils/selector' -import ReceiveModal from './ReceiveModal' -import TermModal from './TermModal' +import { ConnectWalletModal } from 'src/components/ConnectWalletModal' import { useSidebarItems } from 'src/layout/Sidebar/useSidebarItems' import useAddressBookSync from 'src/logic/addressBook/hooks/useAddressBookSync' -import { extractSafeAddress, extractSafeId } from 'src/routes/routes' -import loadSafesFromStorage from 'src/logic/safe/store/actions/loadSafesFromStorage' import loadCurrentSessionFromStorage from 'src/logic/currentSession/store/actions/loadCurrentSessionFromStorage' -import { ConnectWalletModal } from 'src/components/ConnectWalletModal' +import loadSafesFromStorage from 'src/logic/safe/store/actions/loadSafesFromStorage' +import { extractSafeAddress, extractSafeId } from 'src/routes/routes' +import TermModal from './TermModal' import TermContext from 'src/logic/TermContext' import { fetchAllValidator } from 'src/logic/validator/store/actions' -import { fetchAllDelegations } from 'src/logic/delegation/store/actions' const notificationStyles = { success: { @@ -103,8 +100,6 @@ const App: React.FC = ({ children }) => { termContext?.SetTerm(false) } - // Load the Safes from LS just once, - // they'll be reloaded on network change useEffect(() => { dispatch(loadSafesFromStorage()) dispatch(loadCurrentSessionFromStorage()) @@ -150,25 +145,6 @@ const App: React.FC = ({ children }) => { onClose={onConnectWalletHide} > - - - {addressFromUrl && ( - - - - )} - diff --git a/src/components/AddressInfo/index.tsx b/src/components/AddressInfo/index.tsx index 3a03417ce4..b10835c840 100644 --- a/src/components/AddressInfo/index.tsx +++ b/src/components/AddressInfo/index.tsx @@ -62,7 +62,7 @@ const DEFAULT_PROPS: AddressEx = { name: null, logoUri: null, } -export const useKnownAddress = (props: AddressEx | null = DEFAULT_PROPS): AddressEx & { isInAddressBook: boolean } => { +const useKnownAddress = (props: AddressEx | null = DEFAULT_PROPS): AddressEx & { isInAddressBook: boolean } => { const recipientName = useSelector((state) => addressBookEntryName(state, { address: props?.value || '' })) // Undefined known address diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx index 3af45a59b0..f81de4e4b5 100644 --- a/src/components/Button/index.tsx +++ b/src/components/Button/index.tsx @@ -9,7 +9,7 @@ export const OutlinedButton = ({ children, ...rest }) => { ) } -export const OutlinedButtonWrap = styled.button<{ disabled?: boolean }>` +const OutlinedButtonWrap = styled.button<{ disabled?: boolean }>` cursor: pointer; border: 1px solid transparent; background-image: ${borderLinear}; @@ -84,7 +84,7 @@ export const OutlinedNeutralButton = ({ children, ...rest }) => { ) } -export const OutlinedNeutralButtonWrap = styled.button<{ disabled?: boolean; color?: string }>` +const OutlinedNeutralButtonWrap = styled.button<{ disabled?: boolean; color?: string }>` background: transparent; cursor: pointer; padding: 0 !important; diff --git a/src/components/CardStaking/index.tsx b/src/components/CardStaking/index.tsx deleted file mode 100644 index 2c22ee8486..0000000000 --- a/src/components/CardStaking/index.tsx +++ /dev/null @@ -1,134 +0,0 @@ -import { Text } from '@aura/safe-react-components' -import { makeStyles } from '@material-ui/core' -import { ReactElement, useEffect, useState } from 'react' -import TableVoting, { StyledTableCell, StyledTableRow } from 'src/components/TableVoting' -import { - BoxCardStaking, - BoxCardStakingList, - BoxCardStakingOverview, - ImgRow, - StyledButton, - StyledButtonManage, - styles, -} from './style' - -const RowHead = [ - { name: 'NAME' }, - { name: 'AMOUNT STAKED' }, - { name: 'PENDING REWARD' }, - // { name: 'TIME' }, - { name: '' }, -] - -const TableVotingDetailInside = (props) => { - const { handleModal, data, nativeCurrency } = props - - return ( - - {data?.map((row, index) => ( - - - - - - {row.validator} - - - - - - {row?.balance?.amount / 10 ** nativeCurrency.decimals} - - - {row?.reward?.length > 0 ? 'Yes' : 'No'} - {/* -
2 months ago
-
{row.voting}
-
*/} - - handleModal(row)}> - - Manage - - - -
- ))} -
- ) -} - -const useStyles = makeStyles(styles) - -function CardStaking(props): ReactElement { - const { - handleModal, - availableBalance, - totalStake, - rewardAmount, - validatorOfUser, - allValidator, - ClaimReward, - nativeCurrency, - } = props - const classes = useStyles() - const [data, setData] = useState() - - useEffect(() => { - const dataTemp: any = [] - allValidator?.map((item) => { - validatorOfUser?.map((i) => { - if (item.operatorAddress === i.operatorAddress) { - dataTemp.push({ - ...item, - balance: i.balance, - reward: i.reward, - }) - } - }) - }) - setData(dataTemp) - }, [validatorOfUser]) - - return ( - - -
-
- - Available Balance: - - - {availableBalance?.amount ? availableBalance?.amount / 10 ** nativeCurrency.decimals : 0}{' '} - {nativeCurrency.symbol} - -
-
- - Total Staked: - - - {totalStake?.amount ? totalStake?.amount / 10 ** nativeCurrency.decimals : 0}{' '} - {nativeCurrency.symbol} - -
-
- {rewardAmount[0]?.amount / 10 ** nativeCurrency.decimals > 0 && ( - - - Claim Reward: {rewardAmount[0] ? (rewardAmount[0]?.amount / 10 ** nativeCurrency.decimals).toFixed(6) : 0}{' '} - {nativeCurrency.symbol} - - - )} -
- {validatorOfUser && validatorOfUser?.length > 0 && ( - - - - )} -
- ) -} - -export default CardStaking diff --git a/src/components/CardStaking/style.ts b/src/components/CardStaking/style.ts deleted file mode 100644 index 81c710686e..0000000000 --- a/src/components/CardStaking/style.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { createStyles } from '@material-ui/core' -import { borderLinear } from 'src/theme/variables' -import styled from 'styled-components' -import { Text, Button } from '@aura/safe-react-components' - -export const styles = createStyles({ - stakingOverview: { - display: 'flex', - flexDirection: 'column', - alignItems: 'flex-start', - gap: '8px', - }, - stakingOverviewTextContainer: { - display: 'flex', - flexDirection: 'row', - justifyContent: 'space - between', - alignItems: 'center', - gap: '8px', - }, -}) - -const StyledButton = styled.div` - display: flex; - border-radius: 24px; - justify-content: center; - align-items: center; - padding: 8px 20px; - gap: 6px; - background: linear-gradient(108.46deg, #5ee6d0 12.51%, #bfc6ff 51.13%, #ffba69 87.49%); - cursor: pointer; - color: rgba(19, 20, 25, 1); -` -const BoxCardStakingOverview = styled.div` - display: flex; - justify-content: space-between; - flex-direction: row; - align-self: stretch; - align-items: center; - padding: 24px; - background: #363843; - border-radius: 25px; -` -const BoxCardStaking = styled.div` - display: flex; - flex-direction: column; - border-radius: 12px; - background: #363843; - align-items: flex-start; - overflow: hidden; -` -const BoxCardStakingList = styled.div` - display: flex; - flex-direction: column; - align-self: stretch; - align-items: flex-start; - padding: 24px; - background: #24262e; -` - -const StyledButtonManage = styled(Button)` - border: 2px solid transparent; - background-image: ${borderLinear}; - background-origin: border-box; - background-clip: content-box, border-box; - border-radius: 50px !important; - padding: 0 !important; - background-color: transparent !important; - min-width: 130px !important; -` - -const HeaderValidator = styled.div` - display: flex; - justify-content: space-between; - width: 100%; -` -const ImgRow = styled.div` - display: flex; - > img { - margin-right: 5px; - width: 24px; - height: 24px; - border-radius: 50%; - } -` -export { - StyledButton, - BoxCardStakingOverview, - BoxCardStaking, - BoxCardStakingList, - StyledButtonManage, - HeaderValidator, - ImgRow, -} diff --git a/src/components/CardVoting/index.tsx b/src/components/CardVoting/index.tsx deleted file mode 100644 index b639c90459..0000000000 --- a/src/components/CardVoting/index.tsx +++ /dev/null @@ -1,204 +0,0 @@ -import { Button, Text } from '@aura/safe-react-components' -import { ReactElement } from 'react' -import { generatePath, useHistory } from 'react-router-dom' -import Col from 'src/components/layout/Col' -import { getPrefixedSafeAddressSlug, SAFE_ADDRESS_SLUG, SAFE_ROUTES, VOTING_ID_NUMBER } from 'src/routes/routes' -import { borderLinear } from 'src/theme/variables' -import { IProposal, VoteMapping } from 'src/types/proposal' -import { formatDateTimeDivider } from 'src/utils/date' -import styled from 'styled-components' -import BoxCard from '../BoxCard' -import StatusCard from '../StatusCard' -import VoteBar from '../Vote' - -const TitleNumberStyled = styled.div` - font-weight: 510; - font-size: 20px; - line-height: 26px; - color: #b4b8c0; -` - -const TitleStyled = styled.div` - font-weight: 510; - font-size: 20px; - line-height: 26px; - color: rgba(255, 255, 255, 1); -` - -const ContentCard = styled.div` - display: flex; - justify-content: space-between; - margin-top: 10px; -` - -const TitleContentCard = styled.div` - font-weight: 510; - font-size: 14px; - color: #868a97; -` - -const TextStyled = styled(Text)` - color: #b4b8c0; -` - -const StyledButton = styled(Button)` - border: 2px solid transparent; - background-image: ${borderLinear}; - background-origin: border-box; - background-clip: content-box, border-box; - border-radius: 50px !important; - padding: 0 !important; - background-color: transparent !important; - min-width: 130px !important; - margin-left: 10px; - &:disabled { - cursor: not-allowed; - pointer-events: unset; - } -` - -const StyledButtonDetail = styled(Button)` - border: 1px solid #5c606d; - border-radius: 50px !important; - padding: 0 !important; - min-width: 130px !important; - background-color: transparent !important; -` - -const ContainDotVot = styled.div` - display: flex; - justify-content: space-between; - margin-top: 6px; -` - -const DotVoteStyled = styled.div` - width: 16px; - height: 16px; - background: #5ee6d0; - background: ${(props) => props.color ?? '#5ee6d0'}; - border-radius: 23px; - margin-right: 10px; -` - -interface Props { - handleVote?: () => void - proposal: IProposal -} - -const formatTime = (time) => formatDateTimeDivider(new Date(time).getTime()) - -function CardVoting({ handleVote, proposal }: Props): ReactElement { - const history = useHistory() - - const handleDetail = (proposalId) => { - const proposalDetailsPathname = generatePath(SAFE_ROUTES.VOTING_DETAIL, { - [SAFE_ADDRESS_SLUG]: getPrefixedSafeAddressSlug(), - [VOTING_ID_NUMBER]: proposalId, - }) - history.push(proposalDetailsPathname) - } - - const proposalMostVotedOnName = proposal.tally.mostVotedOn.name - const isEnded = new Date(proposal.votingEnd).getTime() < Date.now() - - return ( - - -
-
- #{proposal.id} - {proposal.title} -
-
- -
-
- - - - Proposer - - {proposal.proposer || '-'} - - - - - Voting Start - - {formatTime(proposal.votingStart)} - - - - - Voting End - - {formatTime(proposal.votingEnd)} - - - - - - - - - -
- {' '} - Most voted on -
- -
- - - {VoteMapping[proposalMostVotedOnName || 'yes']} - -
-
- - {proposal.tally.mostVotedOn.percent}% - -
-
- - - -
-
- {isEnded && ( - - Voting ended - - )} -
-
- handleDetail(proposal.id)}> - - Details - - - - - - Vote - - -
-
- -
- ) -} - -export default CardVoting diff --git a/src/components/ConnectWalletModal/styles.tsx b/src/components/ConnectWalletModal/styles.tsx index c1eb33a963..1fc4966cb9 100644 --- a/src/components/ConnectWalletModal/styles.tsx +++ b/src/components/ConnectWalletModal/styles.tsx @@ -3,7 +3,7 @@ import styled from 'styled-components' import { lg } from '../../theme/variables' import Row from '../layout/Row' -export const styles = createStyles({ +const styles = createStyles({ heading: { padding: lg, justifyContent: 'space-between', diff --git a/src/components/CustomTransactionMessage/BigMsg.tsx b/src/components/CustomTransactionMessage/BigMsg.tsx index b842db1865..98bac22557 100644 --- a/src/components/CustomTransactionMessage/BigMsg.tsx +++ b/src/components/CustomTransactionMessage/BigMsg.tsx @@ -1,8 +1,7 @@ import { Accordion, AccordionSummary, AccordionDetails } from '@aura/safe-react-components' import { beutifyJson } from 'src/utils' import styled from 'styled-components' - -export const NoPaddingAccordion = styled(Accordion)` +const NoPaddingAccordion = styled(Accordion)` margin-bottom: 16px !important; border-radius: 8px !important; &.MuiAccordion-root { @@ -13,7 +12,7 @@ export const NoPaddingAccordion = styled(Accordion)` } ` -export const StyledAccordionSummary = styled(AccordionSummary)` +const StyledAccordionSummary = styled(AccordionSummary)` background-color: #363843 !important; border: none !important; height: 52px; @@ -25,7 +24,7 @@ export const StyledAccordionSummary = styled(AccordionSummary)` min-width: 80px; } ` -export const StyledAccordionDetails = styled(AccordionDetails)` +const StyledAccordionDetails = styled(AccordionDetails)` padding: 16px !important; background: #34353a !important; ; ` diff --git a/src/components/CustomTransactionMessage/SmallMsg.tsx b/src/components/CustomTransactionMessage/SmallMsg.tsx index dbf2accd01..c674d991a9 100644 --- a/src/components/CustomTransactionMessage/SmallMsg.tsx +++ b/src/components/CustomTransactionMessage/SmallMsg.tsx @@ -2,7 +2,7 @@ import { Accordion, AccordionDetails, AccordionSummary } from '@aura/safe-react- import { beutifyJson } from 'src/utils' import styled from 'styled-components' -export const NoPaddingAccordion = styled(Accordion)` +const NoPaddingAccordion = styled(Accordion)` margin-bottom: 8px !important; border-radius: 4px !important; &.MuiAccordion-root { @@ -13,7 +13,7 @@ export const NoPaddingAccordion = styled(Accordion)` } ` -export const StyledAccordionSummary = styled(AccordionSummary)` +const StyledAccordionSummary = styled(AccordionSummary)` background-color: #363843 !important; border: none !important; min-height: 24px !important; @@ -30,7 +30,7 @@ export const StyledAccordionSummary = styled(AccordionSummary)` margin: 0px !important; } ` -export const StyledAccordionDetails = styled(AccordionDetails)` +const StyledAccordionDetails = styled(AccordionDetails)` padding: 8px !important; background: #34353a !important; font-size: 12px !important; diff --git a/src/components/DetailVoting/index.tsx b/src/components/DetailVoting/index.tsx deleted file mode 100644 index 850f332e0a..0000000000 --- a/src/components/DetailVoting/index.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { ReactElement, useEffect, useState } from 'react' -import styled from 'styled-components' - -import Markdown from 'marked-react' - -const DetailWrapper = styled.div` - display: flex; - width: 100%; - flex-direction: column; - align-items: flex-start; - border-radius: 8px; - padding: 16px; - background-color: rgba(19, 20, 25, 0.5); - max-height: 360px; - overflow-y: auto; - - color: #b4b8c0; - - & a { - color: #5ee6d0; - } - - & h1, - & h2, - & h3, - & h4, - & h5, - & h6 { - color: #e6e7e8; - } -` - -interface Props { - description: string -} - -function DetailVoting({ description }: Props): ReactElement { - const [markDown, setMarkDown] = useState(description) - useEffect(() => { - setMarkDown(description.replace(/<\/div>/gi, '')) - }, [description]) - return ( - - - - ) -} - -export default DetailVoting diff --git a/src/components/Divider/divider.stories.tsx b/src/components/Divider/divider.stories.tsx deleted file mode 100644 index ad9534188d..0000000000 --- a/src/components/Divider/divider.stories.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import Divider from './index' - -export default { - title: 'Data Display/Divider', - component: Divider, - parameters: { - componentSubtitle: 'Used to separate content.', - }, -} - -export const Horizontal = (): React.ReactElement => ( - <> -
Some content
- -
Some content2
- -) - -export const Arrow = (): React.ReactElement => ( - <> -
Some content
- -
Some content2
- -) diff --git a/src/components/InfiniteScroll/index.tsx b/src/components/InfiniteScroll/index.tsx index 75be255e7b..8ad11453c9 100644 --- a/src/components/InfiniteScroll/index.tsx +++ b/src/components/InfiniteScroll/index.tsx @@ -3,13 +3,13 @@ import { InViewHookResponse, useInView } from 'react-intersection-observer' export const INFINITE_SCROLL_CONTAINER = 'infinite-scroll-container' -export const InfiniteScrollContext = createContext<{ +const InfiniteScrollContext = createContext<{ ref: MutableRefObject | ((instance: HTMLDivElement | null) => void) | null lastItemId?: string setLastItemId: (itemId?: string) => void }>({ setLastItemId: () => {}, ref: null }) -export const InfiniteScrollProvider = forwardRef( +const InfiniteScrollProvider = forwardRef( ({ children }, ref): ReactElement => { const [lastItemId, _setLastItemId] = useState() diff --git a/src/components/Input/Address/style.tsx b/src/components/Input/Address/style.tsx deleted file mode 100644 index 502b119d8a..0000000000 --- a/src/components/Input/Address/style.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import styled from 'styled-components' - -export const Wrapper = styled.div`` diff --git a/src/components/Input/AmountInput/index.tsx b/src/components/Input/AmountInput/index.tsx index 031b5ab623..5e20ab7cc6 100644 --- a/src/components/Input/AmountInput/index.tsx +++ b/src/components/Input/AmountInput/index.tsx @@ -7,7 +7,7 @@ import { colorLinear } from 'src/theme/variables' import { formatNumber, isNumberKeyPress } from 'src/utils' import styled from 'styled-components' -export const StyledTextField = styled(MuiTextField)` +const StyledTextField = styled(MuiTextField)` width: 100%; > label { z-index: 1; diff --git a/src/components/JsonschemaForm/utils.tsx b/src/components/JsonschemaForm/utils.tsx index 353fca124d..65f5932fbb 100644 --- a/src/components/JsonschemaForm/utils.tsx +++ b/src/components/JsonschemaForm/utils.tsx @@ -18,7 +18,7 @@ export function makeSchemaInput(validator: Validator): any[] { } } -export function getProperties(schema: Schema, validator: Validator) { +function getProperties(schema: Schema, validator: Validator) { try { const fieldName = Object.keys(schema.properties || {}).at(0) if (!fieldName) throw new Error('No property') diff --git a/src/components/List/ListItemText/index.tsx b/src/components/List/ListItemText/index.tsx deleted file mode 100644 index b1635895e2..0000000000 --- a/src/components/List/ListItemText/index.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import MuiListItemText from '@material-ui/core/ListItemText' -import { withStyles } from '@material-ui/core/styles' -import * as React from 'react' - -const styles = { - itemTextSecondary: { - textOverflow: 'ellipsis', - overflow: 'hidden', - whiteSpace: 'nowrap', - }, -} - -class ListItemText extends React.PureComponent { - render() { - const { classes, cut = false, primary, secondary } = this.props - - const cutStyle = cut - ? { - secondary: classes.itemTextSecondary, - } - : undefined - - return - } -} - -export default withStyles(styles as any)(ListItemText) diff --git a/src/components/Modal/index.stories.tsx b/src/components/Modal/index.stories.tsx deleted file mode 100644 index 0e3c3557a7..0000000000 --- a/src/components/Modal/index.stories.tsx +++ /dev/null @@ -1,174 +0,0 @@ -import { Text } from '@aura/safe-react-components' -import { ReactElement, useState } from 'react' - -import TextField from 'src/components/forms/TextField' -import GnoField from 'src/components/forms/Field' -import GnoForm from 'src/components/forms/GnoForm' -import { required } from 'src/components/forms/validator' - -import { Modal } from '.' - -export default { - title: 'Modal', - component: Modal, - parameters: { - children: 'The body of the modal or the whole modal being composed by `Modal.Header` and `Modal.Footer` components', - title: 'The title, useful for screen readers', - description: 'A description, useful for screen readers', - handleClose: - 'A callback which will be called when an action to close the modal is triggered (Esc, clicking outside, etc)', - open: 'If `true`, the modal will be displayed. Hidden otherwise.', - }, - compositionElements: [ - { - title: 'Modal.Header', - component: , - parameters: { - title: 'The title that will be displayed in the modal', - titleNote: 'An annotation for the title, like "1 of 2"', - onClose: 'Callback to be called when attempt to close the modal', - }, - compositionElements: [ - { - title: 'Modal.Header.Title', - component: {}, - description: 'safe-react-component exposed with a few styles added to personalize the modal header', - }, - ], - }, - { - title: 'Modal.Header', - component: {}, - parameters: { - children: 'whatever is required to be rendered in the footer. Usually buttons.', - noPadding: 'a flag that will set padding to 0 (zero) in case it is needed', - }, - }, - { - title: 'Modal.Footer', - component: {}, - parameters: { - children: 'whatever is required to be rendered in the footer. Usually buttons.', - }, - compositionElements: [ - { - title: 'Modal.Footer.Buttons', - component: , - description: 'standard two buttons wrapped implementation. One "Cancel" and one "Submit" button.', - }, - ], - }, - ], -} - -const SimpleFormModal = ({ title, description, handleClose, handleSubmit, isOpen, children }) => ( - - {/* header */} - - {title} - - - - {() => ( - <> - {/* body */} - {children} - - {/* footer */} - - - - - )} - - -) - -const Username = () => ( - -) - -export const FormModal = (): ReactElement => { - const [isOpen, setIsOpen] = useState(false) - - const handleClose = () => { - setIsOpen(false) - console.log('modal closed') - } - - const handleSubmit = (values) => { - alert(JSON.stringify(values, null, 2)) - console.log('form submitted', values) - handleClose() - } - - return ( -
- - {/* Modal with Form */} - - {/* Form Fields */} - - -
- ) -} - -export const RemoveSomething = (): ReactElement => { - const [isOpen, setIsOpen] = useState(false) - const title = 'Remove Something' - - const handleClose = () => { - setIsOpen(false) - console.log('modal closed') - } - - const handleSubmit = () => { - alert('Something was removed') - handleClose() - } - - return ( -
- - {/* Modal */} - - {/* Header */} - - {title} - - - {/* Body */} - - You are about to remove something - - - {/* Footer */} - - - - -
- ) -} diff --git a/src/components/Modal/type.tsx b/src/components/Modal/type.tsx index 45fb1ec556..e6710c9635 100644 --- a/src/components/Modal/type.tsx +++ b/src/components/Modal/type.tsx @@ -2,7 +2,7 @@ import { ButtonProps as ButtonPropsMUI } from '@material-ui/core' import { theme } from '@aura/safe-react-components' import { ReactNode } from 'react' -export type Theme = typeof theme +type Theme = typeof theme export interface BodyProps { children: ReactNode @@ -26,7 +26,7 @@ export interface TitleProps { strong?: boolean } -export type CustomButtonMUIProps = Omit & { +type CustomButtonMUIProps = Omit & { to?: string component?: ReactNode } @@ -37,8 +37,7 @@ export enum ButtonStatus { READY, LOADING, } - -export interface ButtonProps extends CustomButtonMUIProps { +interface ButtonProps extends CustomButtonMUIProps { text?: string status?: ButtonStatus size?: keyof Theme['buttons']['size'] diff --git a/src/components/ModalNew/index.tsx b/src/components/ModalNew/index.tsx deleted file mode 100644 index 615b37bc47..0000000000 --- a/src/components/ModalNew/index.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import Modal from 'react-modal' -import { useEffect } from 'react' -const customStyles = { - overlay: { - backgroundColor: 'rgba(35, 38, 47, 0.3)', - }, - content: { - backgroundColor: '#131419', - borderRadius: '12px', - border: 'none', - top: '45%', - left: '50%', - right: 'auto', - bottom: 'auto', - marginRight: '-50%', - transform: 'translate(-50%, -50%)', - width: 720, - }, -} - -export default function ModalNew(props) { - const { title, closeModal, modalIsOpen, children } = props - - useEffect(() => { - Modal.setAppElement('body') - }, []) - - return ( - -
{children}
-
- ) -} diff --git a/src/components/Popup/MultiSendPopup/styles.tsx b/src/components/Popup/MultiSendPopup/styles.tsx index 623ed9603e..807df29b0f 100644 --- a/src/components/Popup/MultiSendPopup/styles.tsx +++ b/src/components/Popup/MultiSendPopup/styles.tsx @@ -3,42 +3,6 @@ import styled from 'styled-components' export const PopupWrapper = styled.div` width: 640px; ` -export const HeaderWrapper = styled.div` - padding: 14px 24px; - display: flex; - justify-content: space-between; - align-items: center; - width: 640px; - border-bottom: 1px solid #404047; - > div:nth-child(2) { - display: flex; - color: #98989b; - font-weight: 600; - font-size: 14px; - line-height: 18px; - > span:nth-child(1) { - margin-right: 16px; - } - } - .title { - font-weight: 600; - font-size: 20px; - line-height: 24px; - margin: 0; - } - .sub-title { - font-weight: 400; - font-size: 14px; - line-height: 18px; - letter-spacing: 0.01em; - color: #98989b; - margin: 2px 0px 0px 0px; - } - .close-icon { - color: #98989b !important; - cursor: pointer; - } -` export const BodyWrapper = styled.div` padding: 24px; .recipient-input { diff --git a/src/components/Popup/SendingPopup/index.tsx b/src/components/Popup/SendingPopup/index.tsx index d4083c6d1c..4d6e49808c 100644 --- a/src/components/Popup/SendingPopup/index.tsx +++ b/src/components/Popup/SendingPopup/index.tsx @@ -28,15 +28,6 @@ import CurrentSafe from './CurrentSafe' import { BodyWrapper, Footer, PopupWrapper } from './styles' import Loader from 'src/components/Loader' -export type SendFundsTx = { - amount?: string - recipientAddress?: string - name?: string - token?: string - txType?: string - tokenSpendingLimit?: SpendingLimit -} - type SendFundsProps = { open: boolean onClose: () => void diff --git a/src/components/Popup/SendingPopup/styles.tsx b/src/components/Popup/SendingPopup/styles.tsx index 90986dc034..ea43ae797b 100644 --- a/src/components/Popup/SendingPopup/styles.tsx +++ b/src/components/Popup/SendingPopup/styles.tsx @@ -3,42 +3,7 @@ import styled from 'styled-components' export const PopupWrapper = styled.div` width: 480px; ` -export const HeaderWrapper = styled.div` - padding: 14px 24px; - display: flex; - justify-content: space-between; - align-items: center; - width: 640px; - border-bottom: 1px solid #404047; - > div:nth-child(2) { - display: flex; - color: #98989b; - font-weight: 600; - font-size: 14px; - line-height: 18px; - > span:nth-child(1) { - margin-right: 16px; - } - } - .title { - font-weight: 600; - font-size: 20px; - line-height: 24px; - margin: 0; - } - .sub-title { - font-weight: 400; - font-size: 14px; - line-height: 18px; - letter-spacing: 0.01em; - color: #98989b; - margin: 2px 0px 0px 0px; - } - .close-icon { - color: #98989b !important; - cursor: pointer; - } -` + export const BodyWrapper = styled.div` padding: 24px; .label { diff --git a/src/components/Stepper/Stepper.tsx b/src/components/Stepper/Stepper.tsx index 8079f6a030..15fe7fbe20 100644 --- a/src/components/Stepper/Stepper.tsx +++ b/src/components/Stepper/Stepper.tsx @@ -129,8 +129,7 @@ export default function Stepper(props: StepperProps): ReactElement { } const useStyles = makeStyles((theme) => styles(theme)) - -export type StepElementProps = { +type StepElementProps = { label: string children: JSX.Element nextButtonLabel?: string diff --git a/src/components/StepperForm/StepperForm.tsx b/src/components/StepperForm/StepperForm.tsx index d29bdf3974..9100fc44b3 100644 --- a/src/components/StepperForm/StepperForm.tsx +++ b/src/components/StepperForm/StepperForm.tsx @@ -63,14 +63,14 @@ function StepperForm({ children, onSubmit, testId, initialValues }: StepperFormP export default StepperForm -export type StepFormElementProps = { +type StepFormElementProps = { label: string validate?: (values) => Record | Promise> nextButtonLabel?: string children: ReactElement> disableNextButton?: boolean } -export type StepFormElementType = (props: StepFormElementProps) => StepElementType[] + export function StepFormElement({ children }: StepFormElementProps): ReactElement { return children diff --git a/src/components/SuspenseContainer/index.tsx b/src/components/SuspenseContainer/index.tsx deleted file mode 100644 index fdf3414109..0000000000 --- a/src/components/SuspenseContainer/index.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { Loader } from '@aura/safe-react-components' -import { makeStyles } from '@material-ui/core/styles' -import { ReactElement, Suspense, useEffect, useState } from 'react' - -const useStyles = makeStyles({ - loaderStyle: { - height: '500px', - width: '100%', - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - }, -}) - -type Props = { - children: ReactElement -} - -const SuspenseContainer = ({ children }: Props): React.ReactElement => { - const classes = useStyles() - - return ( - - - - } - > - {children} - - ) -} - -export default SuspenseContainer diff --git a/src/components/Table/sorting.ts b/src/components/Table/sorting.ts index 118af55525..eec306f6b2 100644 --- a/src/components/Table/sorting.ts +++ b/src/components/Table/sorting.ts @@ -1,8 +1,8 @@ import { List } from 'immutable' -export const FIXED = 'fixed' +const FIXED = 'fixed' -export const buildOrderFieldFrom = (attr: string): string => `${attr}Order` +const buildOrderFieldFrom = (attr: string): string => `${attr}Order` const desc = ( a: string, diff --git a/src/components/TableVoting/index.tsx b/src/components/TableVoting/index.tsx deleted file mode 100644 index c77ffb89dc..0000000000 --- a/src/components/TableVoting/index.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { ReactElement } from 'react' -import styled from 'styled-components' - -import { withStyles, makeStyles } from '@material-ui/core/styles' - -import Table from '@material-ui/core/Table' -import TableBody from '@material-ui/core/TableBody' -import TableCell from '@material-ui/core/TableCell' -import TableContainer from '@material-ui/core/TableContainer' -import TableHead from '@material-ui/core/TableHead' -import TableRow from '@material-ui/core/TableRow' -import Pagination from '@material-ui/lab/Pagination' - -export const StyledTableCell = withStyles((theme) => ({ - head: { - backgroundColor: '#363843', - color: '#9DA1AC', - fontSize: 12, - fontWeight: 590, - padding: 8, - letterSpacing: 0, - lineHeight: 1, - }, - body: { - fontSize: '14px !important', - padding: 12, - color: '#E5E7EA !important', - borderTop: '1px solid #363843', - }, -}))(TableCell) - -export const StyledTableRow = styled(TableRow)` - padding: 4px 0px; -` - -const useStyles = makeStyles({ - table: { - minWidth: 700, - marginTop: 20, - borderBottom: '1px solid #363843', - }, - pagi: { - display: 'flex', - marginTop: 20, - justifyContent: 'flex-end', - color: 'white', - }, -}) - -function TableVoting(props): ReactElement { - const { RowHead, children, ShowPaginate } = props - const classes = useStyles() - - return ( - <> - - - - - {RowHead.map((item, index) => { - return ( - - {item.name} - - ) - })} - - - {children} -
- {ShowPaginate && ( - - )} -
- - ) -} - -export default TableVoting diff --git a/src/components/Tabs/NormalTab/TabPanel/index.tsx b/src/components/Tabs/NormalTab/TabPanel/index.tsx deleted file mode 100644 index 98bcc1f679..0000000000 --- a/src/components/Tabs/NormalTab/TabPanel/index.tsx +++ /dev/null @@ -1,22 +0,0 @@ -export function a11yProps(index) { - return { - id: `scrollable-auto-tab-${index}`, - 'aria-controls': `scrollable-auto-tabpanel-${index}`, - } -} - -export default function TabPanel(props) { - const { children, value, index, ...other } = props - - return ( - - ) -} diff --git a/src/components/TextBox/index.tsx b/src/components/TextBox/index.tsx deleted file mode 100644 index b53d4e7ebe..0000000000 --- a/src/components/TextBox/index.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import styled from 'styled-components' - -import { border } from 'src/theme/variables' - -const Box = styled.p` - padding: 10px; - word-wrap: break-word; - border: solid 2px ${border}; -` - -type Props = { - children: React.ReactNode - className?: string -} - -const TextBox = ({ children, ...rest }: Props): React.ReactElement => { - return {children} -} - -export default TextBox diff --git a/src/components/VotingBarDetail/index.tsx b/src/components/VotingBarDetail/index.tsx deleted file mode 100644 index 2cae0640db..0000000000 --- a/src/components/VotingBarDetail/index.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import { ReactElement } from 'react' -import { IProposal, VoteMapping } from 'src/types/proposal' - -import Col from 'src/components/layout/Col' -import VoteBar from 'src/components/Vote' -import Row from 'src/components/layout/Row' -import styled, { css } from 'styled-components' -import { Text } from '@aura/safe-react-components' -import { getCoinDecimal, getCoinSymbol } from 'src/config' -import { calcBalance } from 'src/utils/calc' - -const ContainDotVot = styled.div` - display: flex; - justify-content: space-between; -` - -const DotVoteStyled = styled.div<{ bg: string }>` - width: 16px; - height: 16px; - background: ${(props) => props.bg || '#5ee6d0'}; - border-radius: 23px; -` -const TextValue = styled(Text)` - margin-left: 26px; - margin-top: 6px; -` - -const TextPercent = styled(Text)` - font-size: 22px; - font-weight: bold; - margin-left: 22px; -` - -interface Props { - proposal: IProposal -} - -function VotingBarDetail({ proposal }: Props): ReactElement { - const symbol = getCoinSymbol() - const decimal = getCoinDecimal() - return ( - <> - - - - - - -
- - - {VoteMapping['yes']} - - - - {Number(proposal.tally.yes.percent).toFixed(2)}% - -
-
- - {Number(calcBalance(proposal.tally.yes.number || '0', decimal)).toFixed(6)}{' '} - {symbol} - - - - - -
- - - {VoteMapping['no']} - - - - {Number(proposal.tally.no.percent).toFixed(2)}% - -
-
- - {Number(calcBalance(proposal.tally?.no?.number || '0', decimal)).toFixed(6)}{' '} - {symbol} - - - - - -
- - - {VoteMapping['abstain']} - - - - {Number(proposal.tally.abstain.percent).toFixed(2)}% - -
-
- - {Number(calcBalance(proposal?.tally?.abstain?.number || '0', decimal)).toFixed(6)}{' '} - {symbol} - - - - - -
- - - {VoteMapping['no_with_veto']} - - - - {Number(proposal.tally.mostVotedOn.percent).toFixed(2)}% - -
-
- - {Number(calcBalance(proposal.tally.mostVotedOn.number || '0', decimal)).toFixed(6)}{' '} - {symbol} - - -
- - ) -} - -export default VotingBarDetail diff --git a/src/components/WalletSwitch/styles.tsx b/src/components/WalletSwitch/styles.tsx deleted file mode 100644 index f9bbb7d63f..0000000000 --- a/src/components/WalletSwitch/styles.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { createStyles } from '@material-ui/core' -import { borderLinear } from 'src/theme/variables' - -const styles = createStyles({ - border: { - border: '2px solid transparent', - backgroundImage: borderLinear, - backgroundOrigin: 'border-box', - backgroundClip: 'content-box, border-box', - borderRadius: 50, - }, - ButtonBorder: { - backgroundColor: 'transparent !important', - }, -}) - -export { styles } diff --git a/src/components/forms/AddressInput/index.tsx b/src/components/forms/AddressInput/index.tsx index 324f0aef93..44ceab7f9b 100644 --- a/src/components/forms/AddressInput/index.tsx +++ b/src/components/forms/AddressInput/index.tsx @@ -19,7 +19,7 @@ import { checksumAddress } from 'src/utils/checksumAddress' import { Errors, logError } from 'src/logic/exceptions/CodedException' import { parsePrefixedAddress } from 'src/utils/prefixedAddress' -export interface AddressInputProps { +interface AddressInputProps { fieldMutator: (address: string) => void name?: string text?: string diff --git a/src/components/forms/validator.ts b/src/components/forms/validator.ts index 563daa816f..2b7fa18c4e 100644 --- a/src/components/forms/validator.ts +++ b/src/components/forms/validator.ts @@ -10,7 +10,7 @@ import { isValidPrefix, parsePrefixedAddress } from 'src/utils/prefixedAddress' import { hasFeature } from 'src/logic/safe/utils/safeVersion' type ValidatorReturnType = string | undefined -export type GenericValidatorType = (...args: unknown[]) => ValidatorReturnType +type GenericValidatorType = (...args: unknown[]) => ValidatorReturnType type AsyncValidator = (...args: unknown[]) => Promise export type Validator = GenericValidatorType | AsyncValidator @@ -68,8 +68,6 @@ export const maxValue = return `Maximum value is ${max}` } -export const ok = (): undefined => undefined - export const mustBeHexData = (data: string): ValidatorReturnType => { const isData = getWeb3().utils.isHexStrict(data) @@ -83,7 +81,7 @@ export const mustBeAddressHash = memoize((address: string): ValidatorReturnType return isValidAddress(address) ? undefined : errorMessage }) -export const mustBeValidBech32Address = memoize((address: string, prefix?: string): ValidatorReturnType => { +const mustBeValidBech32Address = memoize((address: string, prefix?: string): ValidatorReturnType => { const errorMessage = 'Must be a valid address' return isValidAddress(address, prefix) ? undefined : errorMessage @@ -186,7 +184,6 @@ export const differentFrom = return undefined } -export const noErrorsOn = (name: string, errors: Record): boolean => errors[name] === undefined export const validAddressBookName = (name: string): string | undefined => { const lengthError = minMaxLength(1, 50)(name) diff --git a/src/components/layout/Backdrop/index.tsx b/src/components/layout/Backdrop/index.tsx deleted file mode 100644 index 2af887a4dd..0000000000 --- a/src/components/layout/Backdrop/index.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import Backdrop from '@material-ui/core/Backdrop' -import { makeStyles } from '@material-ui/core/styles' -import { ReactElement } from 'react' -import ReactDOM from 'react-dom' - -const useStyles = makeStyles({ - root: { - zIndex: 1300, - top: '52px', - }, -}) - -const BackdropLayout = (props: { isOpen: boolean }): ReactElement | null => { - const classes = useStyles() - - if (!props.isOpen) { - return null - } - - return ReactDOM.createPortal(, document.body) -} - -export default BackdropLayout diff --git a/src/components/layout/Pre/index.tsx b/src/components/layout/Pre/index.tsx deleted file mode 100644 index 62aa278ef5..0000000000 --- a/src/components/layout/Pre/index.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import classNames from 'classnames/bind' -import { ReactElement } from 'react' - -import styles from './index.module.scss' - -const cx = classNames.bind(styles) - -const Pre = ({ children, ...props }): ReactElement => ( -
-    {children}
-  
-) - -export default Pre diff --git a/src/components/layout/Table/index.tsx b/src/components/layout/Table/index.tsx index 9545cc0111..2c6139909d 100644 --- a/src/components/layout/Table/index.tsx +++ b/src/components/layout/Table/index.tsx @@ -1,29 +1,4 @@ -import Table from '@material-ui/core/Table' -import TableBody from '@material-ui/core/TableBody' import TableCell from '@material-ui/core/TableCell' -import TableHead from '@material-ui/core/TableHead' import TableRow from '@material-ui/core/TableRow' -import { ReactElement } from 'react' -export { TableBody, TableCell, TableHead, TableRow } - -const buildWidthFrom = (size) => ({ - minWidth: `${size}px`, -}) - -const overflowStyle: any = { - overflowX: 'auto', -} - -// see: https://css-tricks.com/responsive-data-tables/ -const GnoTable = ({ children, size }): ReactElement => { - const style = size ? buildWidthFrom(size) : undefined - - return ( -
- {children}
-
- ) -} - -export default GnoTable +export { TableCell, TableRow } diff --git a/src/layout/Header/components/Layout/styles.tsx b/src/layout/Header/components/Layout/styles.tsx index 21652a1a7c..3b5204230a 100644 --- a/src/layout/Header/components/Layout/styles.tsx +++ b/src/layout/Header/components/Layout/styles.tsx @@ -52,12 +52,7 @@ export const styles = () => ({ }, }) -export const LogoContainer = styled.div` - display: flex; - align-items: center; - justify-content: center; - gap: 12px; -` + export const DevelopBanner = styled.div` color: #868a97; border-radius: 4px; diff --git a/src/layout/Sidebar/DebugToggle/index.tsx b/src/layout/Sidebar/DebugToggle/index.tsx deleted file mode 100644 index b5bd530b6c..0000000000 --- a/src/layout/Sidebar/DebugToggle/index.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import styled from 'styled-components' -import FormControlLabel from '@material-ui/core/FormControlLabel/FormControlLabel' -import { Switch } from '@aura/safe-react-components' -import useCachedState from 'src/utils/storage/useCachedState' -import { LS_USE_PROD_CGW } from 'src/utils/constants' - -const StyledContainer = styled.div` - padding-top: 10px; - margin-left: 16px; -` - -const DebugToggle = (): React.ReactElement => { - const [enabled = false, setEnabled] = useCachedState(LS_USE_PROD_CGW) - - const onToggle = () => { - setEnabled((prev: boolean) => !prev) - - setTimeout(() => { - location.reload() - }, 200) - } - - return ( - - } label="Use prod CGW" /> - - ) -} - -export default DebugToggle diff --git a/src/layout/Sidebar/SafeHeader/index.stories.tsx b/src/layout/Sidebar/SafeHeader/index.stories.tsx deleted file mode 100644 index 11b16666d4..0000000000 --- a/src/layout/Sidebar/SafeHeader/index.stories.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import WalletInfo from './index' - -export default { - title: 'Layout/WalletInfo', - component: WalletInfo, -} - -export const SimpleLayout = (): React.ReactElement => ( - ({})} - onReceiveClick={console.log} - onNewTransactionClick={console.log} - /> -) diff --git a/src/layout/Sidebar/SafeHeader/index.tsx b/src/layout/Sidebar/SafeHeader/index.tsx index d7c1892fff..366a089ea7 100644 --- a/src/layout/Sidebar/SafeHeader/index.tsx +++ b/src/layout/Sidebar/SafeHeader/index.tsx @@ -25,7 +25,7 @@ import { } from './styles' import { Props } from './type' -export const TOGGLE_SIDEBAR_BTN_TESTID = 'TOGGLE_SIDEBAR_BTN' +const TOGGLE_SIDEBAR_BTN_TESTID = 'TOGGLE_SIDEBAR_BTN' const SafeHeader = ({ address, safeName, diff --git a/src/layout/Sidebar/SafeHeader/styles.tsx b/src/layout/Sidebar/SafeHeader/styles.tsx index 1221da0fc4..5e85bc7ef5 100644 --- a/src/layout/Sidebar/SafeHeader/styles.tsx +++ b/src/layout/Sidebar/SafeHeader/styles.tsx @@ -1,9 +1,8 @@ -import { border, fontColor } from 'src/theme/variables' -import styled from 'styled-components' +import { Icon, Text } from '@aura/safe-react-components' import PrefixedEthHashInfo from 'src/components/PrefixedEthHashInfo' -import { Icon, Text, Button } from '@aura/safe-react-components' +import { fontColor } from 'src/theme/variables' +import styled from 'styled-components' import { StyledTextLabelProps } from './type' -import { borderLinear } from 'src/theme/variables' const Container = styled.div` max-width: 320px; @@ -48,19 +47,6 @@ const IconContainer = styled.div` justify-content: center; gap: 6px; ` -const StyledButton = styled(Button)` - border: 2px solid transparent; - background-image: ${borderLinear}; - background-origin: border-box; - background-clip: content-box, border-box; - border-radius: 50px !important; - padding: 0 !important; - background-color: transparent !important; - min-width: 130px !important; - svg { - margin-right: 5px; - } -` const StyledTextLabel = styled(Text)` padding: 8px; @@ -104,9 +90,6 @@ const StyledLabel = styled.div` line-height: 18px; } ` -const StyledText = styled(Text)` - margin: 8px 0 16px 0; -` const StyledDotChainName = styled.span` width: 8px; @@ -116,8 +99,6 @@ const StyledDotChainName = styled.span` margin-right: 5px; ` -const ContainerChainName = styled.div`` - const StyledIdenticonContainer = styled.div` flex-grow: 1; display: flex; @@ -138,14 +119,11 @@ export { IdenticonContainer, StyledIcon, IconContainer, - StyledButton, StyledTextLabel, StyledTextSafeName, StyledPrefixedEthHashInfo, StyledLabel, - StyledText, StyledDotChainName, - ContainerChainName, StyledIdenticonContainer, ContainerButton, StyledTextSafeNameWrapper, diff --git a/src/logic/addressBook/store/reducer/index.ts b/src/logic/addressBook/store/reducer/index.ts index 213b705b1b..944318bb97 100644 --- a/src/logic/addressBook/store/reducer/index.ts +++ b/src/logic/addressBook/store/reducer/index.ts @@ -7,7 +7,7 @@ import { getEntryIndex, hasSameAddressAndChainId, isValidAddressBookName } from export const ADDRESS_BOOK_REDUCER_ID = 'addressBook' -export const initialAddressBookState: AddressBookState = [] +const initialAddressBookState: AddressBookState = [] type Payloads = AddressBookEntry | AddressBookState diff --git a/src/logic/addressBook/store/selectors/index.ts b/src/logic/addressBook/store/selectors/index.ts index 5880da99d8..25e9640a24 100644 --- a/src/logic/addressBook/store/selectors/index.ts +++ b/src/logic/addressBook/store/selectors/index.ts @@ -8,10 +8,6 @@ import { Overwrite } from 'src/types/helpers' export const addressBookState = (state: AppReduxState): AppReduxState['addressBook'] => state['addressBook'] -export const addressBookAddresses = createSelector([addressBookState], (addressBook): string[] => { - return addressBook.map(({ address }) => address) -}) - export type AddressBookMap = { [address: string]: AddressBookEntry } @@ -20,7 +16,7 @@ type AddressBookMapByChain = { [chainId: string]: AddressBookMap } -export const addressBookAsMap = createSelector([addressBookState], (addressBook): AddressBookMapByChain => { +const addressBookAsMap = createSelector([addressBookState], (addressBook): AddressBookMapByChain => { const addressBookMap = {} addressBook.forEach((entry) => { @@ -42,9 +38,6 @@ const getNameByAddress = (addressBook, address: string, chainId: ChainId): strin return addressBook?.[chainId]?.[address]?.name || '' } -const getNameById = (addressBook, id: string, chainId: ChainId): string => { - return addressBook?.[chainId]?.[id]?.name || '' -} type GetNameParams = Overwrite, { address: string }> diff --git a/src/logic/addressBook/utils/index.ts b/src/logic/addressBook/utils/index.ts index f9ae39b2a1..12909832e6 100644 --- a/src/logic/addressBook/utils/index.ts +++ b/src/logic/addressBook/utils/index.ts @@ -3,17 +3,6 @@ import { AddressBookEntry, AddressBookState } from 'src/logic/addressBook/model/ import { sameAddress } from 'src/logic/wallets/ethAddresses' import { AppReduxState } from 'src/logic/safe/store' import { Overwrite } from 'src/types/helpers' - -export type OldAddressBookEntry = { - address: string - name: string - isOwner: boolean -} - -export type OldAddressBookType = { - [safeAddress: string]: [OldAddressBookEntry] -} - export const ADDRESS_BOOK_INVALID_NAMES = ['UNKNOWN', 'OWNER #', 'MY WALLET'] export const isValidAddressBookName = (addressBookName: string): boolean => { diff --git a/src/logic/appearance/actions/setCopyShortName.ts b/src/logic/appearance/actions/setCopyShortName.ts index e3786ccebf..f5ef0dd919 100644 --- a/src/logic/appearance/actions/setCopyShortName.ts +++ b/src/logic/appearance/actions/setCopyShortName.ts @@ -1,7 +1 @@ -import { createAction } from 'redux-actions' - -import { SetCopyShortNamePayload } from '../reducer/appearance' - export const SET_COPY_SHORT_NAME = 'SET_COPY_SHORT_NAME' - -export const setCopyShortName = createAction(SET_COPY_SHORT_NAME) diff --git a/src/logic/appearance/actions/setShowShortName.ts b/src/logic/appearance/actions/setShowShortName.ts index b100ee6568..485f822598 100644 --- a/src/logic/appearance/actions/setShowShortName.ts +++ b/src/logic/appearance/actions/setShowShortName.ts @@ -1,7 +1 @@ -import { createAction } from 'redux-actions' - -import { SetShowShortNamePayload } from '../reducer/appearance' - export const SET_SHOW_SHORT_NAME = 'SET_SHOW_SHORT_NAME' - -export const setShowShortName = createAction(SET_SHOW_SHORT_NAME) diff --git a/src/logic/appearance/selectors/index.ts b/src/logic/appearance/selectors/index.ts deleted file mode 100644 index e554a2578d..0000000000 --- a/src/logic/appearance/selectors/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { APPEARANCE_REDUCER_ID } from 'src/logic/appearance/reducer/appearance' -import { AppReduxState } from 'src/logic/safe/store' - -export const copyShortNameSelector = (state: AppReduxState): boolean => state[APPEARANCE_REDUCER_ID].copyShortName - -export const showShortNameSelector = (state: AppReduxState): boolean => state[APPEARANCE_REDUCER_ID].showShortName diff --git a/src/logic/checkTerm/store/actions/setTerm.ts b/src/logic/checkTerm/store/actions/setTerm.ts index 5a72a917d8..252f245293 100644 --- a/src/logic/checkTerm/store/actions/setTerm.ts +++ b/src/logic/checkTerm/store/actions/setTerm.ts @@ -1,5 +1,5 @@ import { createAction } from 'redux-actions' -import { TermPayload, TermValuePayload } from '../reducer/term' +import { TermPayload } from '../reducer/term' export const SET_TERM = 'SET_TERM' @@ -7,4 +7,3 @@ export const SET_VALUE_TERM = 'SET_VALUE_TERM' export const setTerm = createAction(SET_TERM) -export const setValueTerm = createAction(SET_VALUE_TERM) diff --git a/src/logic/checkTerm/store/reducer/term.ts b/src/logic/checkTerm/store/reducer/term.ts index 4210f6f182..d0b3f23e9b 100644 --- a/src/logic/checkTerm/store/reducer/term.ts +++ b/src/logic/checkTerm/store/reducer/term.ts @@ -15,7 +15,7 @@ export const TermInitialState = { export type TermPayload = { checkTerm: boolean } -export type TermValuePayload = { termValue: any } +type TermValuePayload = { termValue: any } const termReducer = handleActions( { diff --git a/src/logic/collectibles/sources/collectibles.d.ts b/src/logic/collectibles/sources/collectibles.d.ts index 88edabf899..63406937dd 100644 --- a/src/logic/collectibles/sources/collectibles.d.ts +++ b/src/logic/collectibles/sources/collectibles.d.ts @@ -1,16 +1,16 @@ -export interface OpenSeaAssetContract { +interface OpenSeaAssetContract { address: string name: string image_url: string symbol: string } -export interface OpenSeaCollection { +interface OpenSeaCollection { name: string slug: string } -export interface OpenSeaAsset { +interface OpenSeaAsset { asset_contract: OpenSeaAssetContract background_color: string collection: OpenSeaCollection @@ -20,7 +20,7 @@ export interface OpenSeaAsset { token_id: string } -export type OpenSeaAssets = Array +type OpenSeaAssets = Array export interface NFTAsset { address: string diff --git a/src/logic/collectibles/store/selectors/index.ts b/src/logic/collectibles/store/selectors/index.ts index 8ce07e42bb..e9cf4f836f 100644 --- a/src/logic/collectibles/store/selectors/index.ts +++ b/src/logic/collectibles/store/selectors/index.ts @@ -3,8 +3,8 @@ import { NFTAsset, NFTAssets, NFTToken, NFTTokens } from 'src/logic/collectibles import { AppReduxState } from 'src/logic/safe/store' import { NFT_ASSETS_REDUCER_ID, NFT_TOKENS_REDUCER_ID } from 'src/logic/collectibles/store/reducer/collectibles' -export const nftAssets = (state: AppReduxState): NFTAssets => state[NFT_ASSETS_REDUCER_ID] -export const nftTokens = (state: AppReduxState): NFTTokens => state[NFT_TOKENS_REDUCER_ID] +const nftAssets = (state: AppReduxState): NFTAssets => state[NFT_ASSETS_REDUCER_ID] +const nftTokens = (state: AppReduxState): NFTTokens => state[NFT_TOKENS_REDUCER_ID] export const nftAssetsSelector = createSelector(nftAssets, (assets) => assets) diff --git a/src/logic/collectibles/utils/index.ts b/src/logic/collectibles/utils/index.ts index 8b00015134..af0d2e2c8c 100644 --- a/src/logic/collectibles/utils/index.ts +++ b/src/logic/collectibles/utils/index.ts @@ -8,13 +8,13 @@ import { CollectibleTx } from 'src/routes/safe/components/Balances/SendModal/scr // This is an exception made for a popular NFT that's not ERC721 standard-compatible, // so we can allow the user to transfer the assets by using `transferFrom` instead of // the standard `safeTransferFrom` method. -export const CK_ADDRESS = { +const CK_ADDRESS = { [CHAIN_ID.ETHEREUM]: '0x06012c8cf97bead5deae237070f9587f8e7a266d', [CHAIN_ID.RINKEBY]: '0x16baf0de678e52367adc69fd067e5edd1d33e3bf', } // safeTransferFrom(address,address,uint256) -export const SAFE_TRANSFER_FROM_WITHOUT_DATA_HASH = '42842e0e' +const SAFE_TRANSFER_FROM_WITHOUT_DATA_HASH = '42842e0e' /** * Returns a method identifier based on the asset specified and the current network diff --git a/src/logic/config/store/selectors/index.ts b/src/logic/config/store/selectors/index.ts index ca3a84cbe9..c8b22024e4 100644 --- a/src/logic/config/store/selectors/index.ts +++ b/src/logic/config/store/selectors/index.ts @@ -5,7 +5,7 @@ import { ChainId } from 'src/config/chain.d' import { CONFIG_REDUCER_ID } from '../reducer' import { ConfigState } from '../reducer/reducer.d' -export const configState = (state: AppReduxState): ConfigState => state[CONFIG_REDUCER_ID] +const configState = (state: AppReduxState): ConfigState => state[CONFIG_REDUCER_ID] export const currentChainId = createSelector([configState], (config): ChainId => { return config.chainId diff --git a/src/logic/contractInteraction/sources/ABIService/index.ts b/src/logic/contractInteraction/sources/ABIService/index.ts index 9b4117a9e6..efd5d2e6aa 100644 --- a/src/logic/contractInteraction/sources/ABIService/index.ts +++ b/src/logic/contractInteraction/sources/ABIService/index.ts @@ -1,6 +1,6 @@ import { AbiItem, keccak256 } from 'web3-utils' -export interface AllowedAbiItem extends AbiItem { +interface AllowedAbiItem extends AbiItem { name: string type: 'function' } @@ -11,33 +11,26 @@ export interface AbiItemExtended extends AllowedAbiItem { signatureHash: string } -export const getMethodSignature = ({ inputs, name }: AbiItem): string => { +const getMethodSignature = ({ inputs, name }: AbiItem): string => { const params = inputs?.map((x) => x.type).join(',') return `${name}(${params})` } -export const getSignatureHash = (signature: string): string => { +const getSignatureHash = (signature: string): string => { return keccak256(signature).toString() } -export const getMethodHash = (method: AbiItem): string => { - const signature = getMethodSignature(method) - return getSignatureHash(signature) -} - -export const getMethodSignatureAndSignatureHash = ( - method: AbiItem, -): { methodSignature: string; signatureHash: string } => { +const getMethodSignatureAndSignatureHash = (method: AbiItem): { methodSignature: string; signatureHash: string } => { const methodSignature = getMethodSignature(method) const signatureHash = getSignatureHash(methodSignature) return { methodSignature, signatureHash } } -export const isAllowedMethod = ({ name, type }: AbiItem): boolean => { +const isAllowedMethod = ({ name, type }: AbiItem): boolean => { return type === 'function' && !!name } -export const getMethodAction = ({ stateMutability }: AbiItem): 'read' | 'write' => { +const getMethodAction = ({ stateMutability }: AbiItem): 'read' | 'write' => { if (!stateMutability) { return 'write' } diff --git a/src/logic/contracts/api/masterCopies.ts b/src/logic/contracts/api/masterCopies.ts deleted file mode 100644 index 93282ade5e..0000000000 --- a/src/logic/contracts/api/masterCopies.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { MasterCopyReponse, getMasterCopies } from '@gnosis.pm/safe-react-gateway-sdk' - -import { _getChainId } from 'src/config' -import { GATEWAY_URL } from 'src/utils/constants' - -export enum MasterCopyDeployer { - GNOSIS = 'Gnosis', - CIRCLES = 'Circles', -} - -export type MasterCopy = MasterCopyReponse[number] & { - deployer: MasterCopyDeployer - deployerRepoUrl: string -} - -const extractMasterCopyInfo = (mc: MasterCopyReponse[number]): MasterCopy => { - const isCircles = mc.version.toLowerCase().includes(MasterCopyDeployer.CIRCLES.toLowerCase()) - const dashIndex = mc.version.indexOf('-') - - const masterCopy = { - address: mc.address, - version: !isCircles ? mc.version : mc.version.substring(0, dashIndex), - deployer: !isCircles ? MasterCopyDeployer.GNOSIS : MasterCopyDeployer.CIRCLES, - deployerRepoUrl: !isCircles - ? 'https://github.com/gnosis/safe-contracts/releases' - : 'https://github.com/CirclesUBI/safe-contracts/releases', - } - return masterCopy -} - -export const fetchMasterCopies = async (): Promise => { - try { - const res = await getMasterCopies(GATEWAY_URL, _getChainId()) - return res.map(extractMasterCopyInfo) - } catch (error) { - console.error('Fetching data from master-copies errored', error) - } -} diff --git a/src/logic/contracts/methodIds.ts b/src/logic/contracts/methodIds.ts deleted file mode 100644 index 54617c30f5..0000000000 --- a/src/logic/contracts/methodIds.ts +++ /dev/null @@ -1,232 +0,0 @@ -import { - DataDecoded, - SAFE_METHOD_ID_TO_NAME, - SAFE_METHODS_NAMES, - SPENDING_LIMIT_METHOD_ID_TO_NAME, - SPENDING_LIMIT_METHODS_NAMES, - TOKEN_TRANSFER_METHOD_ID_TO_NAME, - TOKEN_TRANSFER_METHODS_NAMES, -} from 'src/logic/safe/store/models/types/transactions.d' -import { getWeb3ReadOnly } from 'src/logic/wallets/getWeb3' -import { sameString } from 'src/utils/strings' - -type DecodeInfoProps = { - paramsHash: string - params: Record -} - -const decodeInfo = ({ paramsHash, params }: DecodeInfoProps): DataDecoded['parameters'] => { - const web3 = getWeb3ReadOnly() - const decodedParameters = web3.eth.abi.decodeParameters(Object.values(params), paramsHash) - - return Object.keys(params).map((name, index) => ({ - name, - type: params[name], - value: decodedParameters[index], - })) -} - -export const decodeParamsFromSafeMethod = (data: string): DataDecoded | undefined => { - const [methodId, paramsHash] = [data.slice(0, 10), data.slice(10)] - const method = SAFE_METHODS_NAMES[methodId] - - switch (method) { - case SAFE_METHODS_NAMES.SWAP_OWNER: { - const params = { - prevOwner: 'address', - oldOwner: 'address', - newOwner: 'address', - } - - // we only need to return the addresses that has been swapped, no need for the `prevOwner` - const [, oldOwner, newOwner] = decodeInfo({ paramsHash, params }) - - return { method, parameters: [oldOwner, newOwner] } - } - - case SAFE_METHODS_NAMES.ADD_OWNER_WITH_THRESHOLD: { - const params = { - owner: 'address', - _threshold: 'uint', - } - - const parameters = decodeInfo({ paramsHash, params }) - - return { method, parameters } - } - - case SAFE_METHODS_NAMES.REMOVE_OWNER: { - const params = { - prevOwner: 'address', - owner: 'address', - _threshold: 'uint', - } - - // we only need to return the removed owner and the new threshold, no need for the `prevOwner` - const [, oldOwner, threshold] = decodeInfo({ paramsHash, params }) - - return { method, parameters: [oldOwner, threshold] } - } - - case SAFE_METHODS_NAMES.CHANGE_THRESHOLD: { - const params = { - _threshold: 'uint', - } - - const parameters = decodeInfo({ paramsHash, params }) - - return { method, parameters } - } - - case SAFE_METHODS_NAMES.ENABLE_MODULE: { - const params = { - module: 'address', - } - - const parameters = decodeInfo({ paramsHash, params }) - - return { method, parameters } - } - - case SAFE_METHODS_NAMES.DISABLE_MODULE: { - const params = { - prevModule: 'address', - module: 'address', - } - - const parameters = decodeInfo({ paramsHash, params }) - - return { method, parameters } - } - - default: - return - } -} - -export const isSetAllowanceMethod = (data: string): boolean => { - const methodId = data.slice(0, 10) - return sameString(SPENDING_LIMIT_METHOD_ID_TO_NAME[methodId], SPENDING_LIMIT_METHODS_NAMES.SET_ALLOWANCE) -} - -export const isDeleteAllowanceMethod = (data: string): boolean => { - const methodId = data.slice(0, 10) - return sameString(SPENDING_LIMIT_METHOD_ID_TO_NAME[methodId], SPENDING_LIMIT_METHODS_NAMES.DELETE_ALLOWANCE) -} - -export const decodeParamsFromSpendingLimit = (data: string): DataDecoded | undefined => { - const [methodId, paramsHash] = [data.slice(0, 10), data.slice(10)] - const method = SPENDING_LIMIT_METHOD_ID_TO_NAME[methodId] - - switch (method) { - case SPENDING_LIMIT_METHODS_NAMES.ADD_DELEGATE: { - const params = { - delegate: 'address', - } - - const parameters = decodeInfo({ paramsHash, params }) - - return { method, parameters } - } - - case SPENDING_LIMIT_METHODS_NAMES.SET_ALLOWANCE: { - const params = { - delegate: 'address', - token: 'address', - allowanceAmount: 'uint96', - resetTimeMin: 'uint16', - resetBaseMin: 'uint32', - } - - const parameters = decodeInfo({ paramsHash, params }) - - return { method, parameters } - } - - case SPENDING_LIMIT_METHODS_NAMES.EXECUTE_ALLOWANCE_TRANSFER: { - const params = { - safe: 'address', - token: 'address', - to: 'address', - amount: 'uint96', - paymentToken: 'address', - payment: 'uint96', - delegate: 'address', - signature: 'bytes', - } - - const parameters = decodeInfo({ paramsHash, params }) - - return { method, parameters } - } - - case SPENDING_LIMIT_METHODS_NAMES.DELETE_ALLOWANCE: { - const params = { - delegate: 'address', - token: 'address', - } - - const parameters = decodeInfo({ paramsHash, params }) - - return { method, parameters } - } - - default: - return - } -} - -const isSafeMethod = (methodId: string): boolean => { - return !!SAFE_METHOD_ID_TO_NAME[methodId] -} - -const isSpendingLimitMethod = (methodId: string): boolean => { - return !!SPENDING_LIMIT_METHOD_ID_TO_NAME[methodId] -} - -export const decodeMethods = (data: string | null): DataDecoded | undefined => { - if (!data?.length) { - return - } - - const [methodId, paramsHash] = [data.slice(0, 10), data.slice(10)] - - if (isSafeMethod(methodId)) { - return decodeParamsFromSafeMethod(data) - } - - if (isSpendingLimitMethod(methodId)) { - return decodeParamsFromSpendingLimit(data) - } - - const method = TOKEN_TRANSFER_METHOD_ID_TO_NAME[methodId] - - switch (method) { - case TOKEN_TRANSFER_METHODS_NAMES.TRANSFER: { - const params = { - to: 'address', - value: 'uint', - } - - const parameters = decodeInfo({ paramsHash, params }) - - return { method, parameters } - } - - case TOKEN_TRANSFER_METHODS_NAMES.TRANSFER_FROM: - case TOKEN_TRANSFER_METHODS_NAMES.SAFE_TRANSFER_FROM: { - const params = { - from: 'address', - to: 'address', - value: 'uint', - } - - const parameters = decodeInfo({ paramsHash, params }) - - return { method, parameters } - } - - default: - return - } -} diff --git a/src/logic/currencyValues/api/fetchAvailableCurrencies.ts b/src/logic/currencyValues/api/fetchAvailableCurrencies.ts deleted file mode 100644 index 441b95c266..0000000000 --- a/src/logic/currencyValues/api/fetchAvailableCurrencies.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { FiatCurrencies, getFiatCurrencies } from '@gnosis.pm/safe-react-gateway-sdk' -import { GATEWAY_URL } from 'src/utils/constants' - -export const fetchAvailableCurrencies = async (): Promise => { - return getFiatCurrencies(GATEWAY_URL) -} diff --git a/src/logic/currencyValues/store/actions/setAvailableCurrencies.ts b/src/logic/currencyValues/store/actions/setAvailableCurrencies.ts index e510b494f5..3c57d5ce54 100644 --- a/src/logic/currencyValues/store/actions/setAvailableCurrencies.ts +++ b/src/logic/currencyValues/store/actions/setAvailableCurrencies.ts @@ -1,6 +1 @@ -import { createAction } from 'redux-actions' -import { AvailableCurrenciesPayload } from 'src/logic/currencyValues/store/reducer/currencyValues' - export const SET_AVAILABLE_CURRENCIES = 'SET_AVAILABLE_CURRENCIES' - -export const setAvailableCurrencies = createAction(SET_AVAILABLE_CURRENCIES) diff --git a/src/logic/currencyValues/store/actions/updateAvailableCurrencies.ts b/src/logic/currencyValues/store/actions/updateAvailableCurrencies.ts deleted file mode 100644 index aa34b4504c..0000000000 --- a/src/logic/currencyValues/store/actions/updateAvailableCurrencies.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Action } from 'redux-actions' -import { ThunkDispatch } from 'redux-thunk' -import { AppReduxState } from 'src/logic/safe/store' -import { AvailableCurrenciesPayload } from 'src/logic/currencyValues/store/reducer/currencyValues' -import { setAvailableCurrencies } from 'src/logic/currencyValues/store/actions/setAvailableCurrencies' -import { fetchAvailableCurrencies } from 'src/logic/currencyValues/api/fetchAvailableCurrencies' -import { Errors, logError } from 'src/logic/exceptions/CodedException' - -export const updateAvailableCurrencies = - () => - async (dispatch: ThunkDispatch>): Promise => { - try { - const availableCurrencies = await fetchAvailableCurrencies() - dispatch(setAvailableCurrencies({ availableCurrencies })) - } catch (err) { - logError(Errors._607, err.message) - } - return Promise.resolve() - } diff --git a/src/logic/currencyValues/store/selectors/index.ts b/src/logic/currencyValues/store/selectors/index.ts index 790a791665..c919cc2bf6 100644 --- a/src/logic/currencyValues/store/selectors/index.ts +++ b/src/logic/currencyValues/store/selectors/index.ts @@ -1,7 +1,5 @@ +import { CURRENCY_REDUCER_ID } from 'src/logic/currencyValues/store/reducer/currencyValues' import { AppReduxState } from 'src/logic/safe/store' -import { CURRENCY_REDUCER_ID, CurrencyValuesState } from 'src/logic/currencyValues/store/reducer/currencyValues' - -export const currencyValuesSelector = (state: AppReduxState): CurrencyValuesState => state[CURRENCY_REDUCER_ID] export const currentCurrencySelector = (state: AppReduxState): string => { return state[CURRENCY_REDUCER_ID].selectedCurrency diff --git a/src/logic/currentSession/store/reducer/currentSession.ts b/src/logic/currentSession/store/reducer/currentSession.ts index 5338e63c0d..9f8f5b82b1 100644 --- a/src/logic/currentSession/store/reducer/currentSession.ts +++ b/src/logic/currentSession/store/reducer/currentSession.ts @@ -16,7 +16,7 @@ export type CurrentSessionState = { restored: boolean } -export const initialState = { +const initialState = { viewedSafes: [], restored: false, } diff --git a/src/logic/delegation/store/actions/index.ts b/src/logic/delegation/store/actions/index.ts index 18a59a57e2..3c4a88820a 100644 --- a/src/logic/delegation/store/actions/index.ts +++ b/src/logic/delegation/store/actions/index.ts @@ -30,4 +30,4 @@ export const fetchAllDelegations = } return Promise.resolve() } -export const setAllDelegation = createAction(FETCH_ALL_DELEGATIONS) +const setAllDelegation = createAction(FETCH_ALL_DELEGATIONS) diff --git a/src/logic/delegation/store/reducer/index.ts b/src/logic/delegation/store/reducer/index.ts index 077386657c..9d858362b6 100644 --- a/src/logic/delegation/store/reducer/index.ts +++ b/src/logic/delegation/store/reducer/index.ts @@ -13,7 +13,7 @@ export type DelegationStateType = { allDelegations: DelegationType[] } -export const initialState = { +const initialState = { allDelegations: [], } diff --git a/src/logic/hooks/useEstimateTransactionGas.tsx b/src/logic/hooks/useEstimateTransactionGas.tsx index 9138c11dee..c3641520f8 100644 --- a/src/logic/hooks/useEstimateTransactionGas.tsx +++ b/src/logic/hooks/useEstimateTransactionGas.tsx @@ -2,22 +2,16 @@ import { Operation } from '@gnosis.pm/safe-react-gateway-sdk' import { List } from 'immutable' import { useEffect, useState } from 'react' import { useSelector } from 'react-redux' -import { fromWei, toWei } from 'web3-utils' +import { fromWei } from 'web3-utils' import { getNativeCurrency } from 'src/config' -import { - checkTransactionExecution, - estimateSafeTxGas, - estimateTransactionGasLimit, -} from 'src/logic/safe/transactions/gas' -import { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue' -import { formatAmount } from 'src/logic/tokens/utils/formatAmount' -import { calculateGasPrice } from 'src/logic/wallets/ethTransactions' +import { checkIfOffChainSignatureIsPossible } from 'src/logic/safe/safeTxSigner' +import { Confirmation } from 'src/logic/safe/store/models/types/confirmation' import { currentSafe } from 'src/logic/safe/store/selectors' +import { estimateSafeTxGas } from 'src/logic/safe/transactions/gas' +import { formatAmount } from 'src/logic/tokens/utils/formatAmount' +import { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue' import { providerSelector } from 'src/logic/wallets/store/selectors' -import { Confirmation } from 'src/logic/safe/store/models/types/confirmation' -import { checkIfOffChainSignatureIsPossible } from 'src/logic/safe/safeTxSigner' -import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses' import { sameString } from 'src/utils/strings' export enum EstimationStatus { @@ -80,7 +74,7 @@ type UseEstimateTransactionGasProps = { manualGasLimit?: string } -export type TransactionGasEstimationResult = { +type TransactionGasEstimationResult = { txEstimationExecutionStatus: EstimationStatus gasEstimation: string // Amount of gas needed for execute or approve the transaction gasCost: string // Cost of gas in raw format (estimatedGas * gasPrice) diff --git a/src/logic/keplr/keplr.ts b/src/logic/keplr/keplr.ts index 424cb8f32b..06497cc6d2 100644 --- a/src/logic/keplr/keplr.ts +++ b/src/logic/keplr/keplr.ts @@ -24,7 +24,7 @@ export type WalletKey = { myPubkey: string } -export enum KeplrErrors { +enum KeplrErrors { Success = 'OK', Failed = 'FAILED', NoChainInfo = 'THERE IS NO CHAIN INFO FOR', diff --git a/src/logic/keplr/useKeplrKeyStoreChange.ts b/src/logic/keplr/useKeplrKeyStoreChange.ts index 1e15a69e85..c8ada3d965 100644 --- a/src/logic/keplr/useKeplrKeyStoreChange.ts +++ b/src/logic/keplr/useKeplrKeyStoreChange.ts @@ -8,7 +8,7 @@ import { loadLastUsedProvider } from 'src/logic/wallets/store/middlewares/provid import { JWT_TOKEN_KEY } from 'src/services/constant/common' import session from 'src/utils/storage/session' -export const useKeplrKeyStoreChange = (): void => { +const useKeplrKeyStoreChange = (): void => { const dispatch = useDispatch() useEffect(() => { diff --git a/src/logic/proposal/store/actions/addOrUpdateProposals.ts b/src/logic/proposal/store/actions/addOrUpdateProposals.ts index 6db36c3b4a..b236f70103 100644 --- a/src/logic/proposal/store/actions/addOrUpdateProposals.ts +++ b/src/logic/proposal/store/actions/addOrUpdateProposals.ts @@ -1,8 +1 @@ -import { createAction } from 'redux-actions' -import { IProposalState } from 'src/logic/proposal/store/reducer/proposals' - export const ADD_OR_UPDATE_PROPOSALS = 'ADD_OR_UPDATE_PROPOSALS' - -const addOrUpdateProposals = createAction(ADD_OR_UPDATE_PROPOSALS) - -export default addOrUpdateProposals diff --git a/src/logic/proposal/store/selectors/index.ts b/src/logic/proposal/store/selectors/index.ts deleted file mode 100644 index 6e54be6f9c..0000000000 --- a/src/logic/proposal/store/selectors/index.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { createSelector } from 'reselect' -import { PROPOSALS_REDUCER_ID } from 'src/logic/proposal/store/reducer/proposals' -import { AppReduxState } from 'src/logic/safe/store' -import { currentChainId } from 'src/logic/config/store/selectors' -import { IProposal } from 'src/types/proposal' -import { extractSafeAddress } from 'src/routes/routes' -import _ from 'lodash' - -const proposalsMapSelector = (state) => state[PROPOSALS_REDUCER_ID] - -export const proposalsListSelector = createSelector(proposalsMapSelector, (proposal) => proposal) - -export const proposals = (state: AppReduxState): AppReduxState['proposals'] => { - return state[PROPOSALS_REDUCER_ID] -} - -export const proposalDetail = createSelector( - proposals, - currentChainId, - extractSafeAddress, - (_: AppReduxState, attrDetail: { attributeName: keyof IProposal; attributeValue: IProposal[keyof IProposal] }) => - attrDetail, - (proposals, chainId, safeAddress, attrDetail): IProposal | undefined => { - const { attributeValue, attributeName } = attrDetail - - const proposalList = chainId && safeAddress ? proposals[chainId]?.[safeAddress] : undefined - - if (!proposalList) { - return undefined - } - - const proposal = proposalList ? _.find(proposalList, (item) => item[attributeName] == attributeValue) : undefined - - return proposal - }, -) diff --git a/src/logic/providers/constants/constant.ts b/src/logic/providers/constants/constant.ts index 9d123cd730..cf1b5d529f 100644 --- a/src/logic/providers/constants/constant.ts +++ b/src/logic/providers/constants/constant.ts @@ -47,4 +47,4 @@ enum MsgTypeUrl { Fail = 'FAILED', } -export { KeplrErrors, WalletProviders, MsgTypeUrl } +export { KeplrErrors, MsgTypeUrl } diff --git a/src/logic/providers/hooks/useKeplrKeyStoreChange.ts b/src/logic/providers/hooks/useKeplrKeyStoreChange.ts deleted file mode 100644 index 1e15a69e85..0000000000 --- a/src/logic/providers/hooks/useKeplrKeyStoreChange.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { useEffect } from 'react' -import { useDispatch } from 'react-redux' - -import { connectKeplr } from 'src/logic/keplr/keplr' -import { Dispatch } from 'src/logic/safe/store/actions/types.d' -import { WALLETS_NAME } from 'src/logic/wallets/constant/wallets' -import { loadLastUsedProvider } from 'src/logic/wallets/store/middlewares/providerWatcher' -import { JWT_TOKEN_KEY } from 'src/services/constant/common' -import session from 'src/utils/storage/session' - -export const useKeplrKeyStoreChange = (): void => { - const dispatch = useDispatch() - - useEffect(() => { - const onKeplrKeyStoreChange = () => { - loadLastUsedProvider().then((lastUsedProvider) => { - if (lastUsedProvider === WALLETS_NAME.Keplr) { - session.removeItem(JWT_TOKEN_KEY) - connectKeplr() - } - }) - } - - window.addEventListener('keplr_keystorechange', onKeplrKeyStoreChange) - - return () => { - window.removeEventListener('keplr_keystorechange', onKeplrKeyStoreChange) - } - }, [dispatch]) -} - -export default useKeplrKeyStoreChange diff --git a/src/logic/providers/txTypes.ts b/src/logic/providers/txTypes.ts deleted file mode 100644 index 08b0e01de5..0000000000 --- a/src/logic/providers/txTypes.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { GeneratedType } from '@cosmjs/proto-signing' -import { MsgSend, MsgMultiSend } from 'cosmjs-types/cosmos/bank/v1beta1/tx' -import { Coin } from 'cosmjs-types/cosmos/base/v1beta1/coin' -import { - MsgFundCommunityPool, - MsgSetWithdrawAddress, - MsgWithdrawDelegatorReward, - MsgWithdrawValidatorCommission, -} from 'cosmjs-types/cosmos/distribution/v1beta1/tx' -import { MsgVote, MsgDeposit, MsgSubmitProposal } from 'cosmjs-types/cosmos/gov/v1beta1/tx' -import { - MsgClearAdmin, - MsgExecuteContract, - MsgMigrateContract, - MsgStoreCode, - MsgInstantiateContract, - MsgUpdateAdmin, -} from 'cosmjs-types/cosmwasm/wasm/v1/tx' -import { - MsgBeginRedelegate, - MsgDelegate, - MsgUndelegate, - MsgCreateValidator, - MsgEditValidator, -} from 'cosmjs-types/cosmos/staking/v1beta1/tx' -import { MsgExec, MsgGrant, MsgRevoke } from 'cosmjs-types/cosmos/authz/v1beta1/tx' -import { MsgGrantAllowance, MsgRevokeAllowance } from 'cosmjs-types/cosmos/feegrant/v1beta1/tx' -import { MsgTransfer } from 'cosmjs-types/ibc/applications/transfer/v1/tx' -import { - MsgAcknowledgement, - MsgChannelCloseConfirm, - MsgChannelCloseInit, - MsgChannelOpenAck, - MsgChannelOpenConfirm, - MsgChannelOpenInit, - MsgChannelOpenTry, - MsgRecvPacket, - MsgTimeout, - MsgTimeoutOnClose, -} from 'cosmjs-types/ibc/core/channel/v1/tx' -import { - MsgCreateClient, - MsgSubmitMisbehaviour, - MsgUpdateClient, - MsgUpgradeClient, -} from 'cosmjs-types/ibc/core/client/v1/tx' -import { - MsgConnectionOpenAck, - MsgConnectionOpenConfirm, - MsgConnectionOpenInit, - MsgConnectionOpenTry, -} from 'cosmjs-types/ibc/core/connection/v1/tx' - -export const TxTypes: Iterable<[string, GeneratedType]> = [ - ['/cosmos.base.v1beta1.Coin', Coin], - ['/cosmos.bank.v1beta1.MsgSend', MsgSend], - ['/cosmos.bank.v1beta1.MsgMultiSend', MsgMultiSend], - ['/cosmos.staking.v1beta1.MsgDelegate', MsgDelegate], - ['/cosmos.staking.v1beta1.MsgBeginRedelegate', MsgBeginRedelegate], - ['/cosmos.staking.v1beta1.MsgUndelegate', MsgUndelegate], - ['/cosmos.staking.v1beta1.MsgCreateValidator', MsgCreateValidator], - ['/cosmos.staking.v1beta1.MsgEditValidator', MsgEditValidator], - ['/cosmos.gov.v1beta1.MsgVote', MsgVote], - ['/cosmos.gov.v1beta1.MsgDeposit', MsgDeposit], - ['/cosmos.gov.v1beta1.MsgSubmitProposal', MsgSubmitProposal], - ['/cosmos.authz.v1beta1.MsgExec', MsgExec], - ['/cosmos.authz.v1beta1.MsgGrant', MsgGrant], - ['/cosmos.authz.v1beta1.MsgRev', MsgRevoke], - ['/cosmos.distribution.v1beta1.MsgFundCommunityPool', MsgFundCommunityPool], - ['/cosmos.distribution.v1beta1.MsgSetWithdrawAddress', MsgSetWithdrawAddress], - ['/cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward', MsgWithdrawDelegatorReward], - ['/cosmos.distribution.v1beta1.MsgWithdrawValidatorCommission', MsgWithdrawValidatorCommission], - ['/cosmos.feegrant.v1beta1.MsgGrantAllowance', MsgGrantAllowance], - ['/cosmos.feegrant.v1beta1.MsgRevokeAllowance', MsgRevokeAllowance], - ['/ibc.applications.transfer.v1.MsgTransfer', MsgTransfer], - ['/ibc.core.channel.v1.MsgAcknowledgement', MsgAcknowledgement], - ['/ibc.core.channel.v1.MsgChannelCloseConfirm', MsgChannelCloseConfirm], - ['/ibc.core.channel.v1.MsgChannelCloseInit', MsgChannelCloseInit], - ['/ibc.core.channel.v1.MsgChannelOpenAck', MsgChannelOpenAck], - ['/ibc.core.channel.v1.MsgChannelOpenConfirm', MsgChannelOpenConfirm], - ['/ibc.core.channel.v1.MsgChannelOpenInit', MsgChannelOpenInit], - ['/ibc.core.channel.v1.MsgChannelOpenTry', MsgChannelOpenTry], - ['/ibc.core.channel.v1.MsgRecvPacket', MsgRecvPacket], - ['/ibc.core.channel.v1.MsgTimeout', MsgTimeout], - ['/ibc.core.channel.v1.MsgTimeoutOnClose', MsgTimeoutOnClose], - ['/ibc.core.client.v1.MsgCreateClient', MsgCreateClient], - ['/ibc.core.client.v1.MsgSubmitMisbehaviour', MsgSubmitMisbehaviour], - ['/ibc.core.client.v1.MsgUpdateClient', MsgUpdateClient], - ['/ibc.core.client.v1.MsgUpgradeClient', MsgUpgradeClient], - ['/ibc.core.connection.v1.MsgConnectionOpenAck', MsgConnectionOpenAck], - ['/ibc.core.connection.v1.MsgConnectionOpenConfirm', MsgConnectionOpenConfirm], - ['/ibc.core.connection.v1.MsgConnectionOpenInit', MsgConnectionOpenInit], - ['/ibc.core.connection.v1.MsgConnectionOpenTry', MsgConnectionOpenTry], - ['/cosmwasm.wasm.v1.MsgClearAdmin', MsgClearAdmin], - ['/cosmwasm.wasm.v1.MsgExecuteContract', MsgExecuteContract], - ['/cosmwasm.wasm.v1.MsgMigrateContract', MsgMigrateContract], - ['/cosmwasm.wasm.v1.MsgStoreCode', MsgStoreCode], - ['/cosmwasm.wasm.v1.MsgInstantiateContract', MsgInstantiateContract], - ['/cosmwasm.wasm.v1.MsgUpdateAdmin', MsgUpdateAdmin], -] diff --git a/src/logic/providers/utils/index.ts b/src/logic/providers/utils/index.ts index 626c469d66..52ab050221 100644 --- a/src/logic/providers/utils/index.ts +++ b/src/logic/providers/utils/index.ts @@ -6,7 +6,7 @@ import { addProvider } from 'src/logic/wallets/store/actions' import { trackAnalyticsEvent, WALLET_EVENTS } from 'src/utils/googleAnalytics' import { makeProvider, ProviderProps } from '../../wallets/store/model/provider' -export function processProviderResponse(dispatch: Dispatch, provider: ProviderProps): void { +function processProviderResponse(dispatch: Dispatch, provider: ProviderProps): void { const walletRecord = makeProvider(provider) dispatch(addProvider(walletRecord)) } @@ -18,7 +18,7 @@ export function fetchProvider(providerInfo: ProviderProps): (dispatch: Dispatch< } } -export function handleProviderNotification(provider: ProviderProps, dispatch: Dispatch): void { +function handleProviderNotification(provider: ProviderProps, dispatch: Dispatch): void { const { available, loaded } = provider if (!loaded) { diff --git a/src/logic/providers/utils/message.ts b/src/logic/providers/utils/message.ts deleted file mode 100644 index 93fb8cd3f4..0000000000 --- a/src/logic/providers/utils/message.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { MsgVoteEncodeObject } from '@cosmjs/stargate' -import { MsgTypeUrl } from 'src/logic/providers/constants/constant' - -const Vote = (value: MsgVoteEncodeObject['value']): MsgVoteEncodeObject => ({ - typeUrl: MsgTypeUrl.Vote, - value: value, -}) - -export { Vote } diff --git a/src/logic/safe/api/fetchSafesByOwner.ts b/src/logic/safe/api/fetchSafesByOwner.ts deleted file mode 100644 index 6246148071..0000000000 --- a/src/logic/safe/api/fetchSafesByOwner.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { getOwnedSafes } from '@gnosis.pm/safe-react-gateway-sdk' - -import { _getChainId } from 'src/config' -import { checksumAddress } from 'src/utils/checksumAddress' -import { GATEWAY_URL } from 'src/utils/constants' - -export const fetchSafesByOwner = async (ownerAddress: string): Promise => { - return getOwnedSafes(GATEWAY_URL, _getChainId(), checksumAddress(ownerAddress)).then(({ safes }) => safes) -} diff --git a/src/logic/safe/store/actions/fetchLatestMasterContractVersion.ts b/src/logic/safe/store/actions/fetchLatestMasterContractVersion.ts deleted file mode 100644 index cf8e209876..0000000000 --- a/src/logic/safe/store/actions/fetchLatestMasterContractVersion.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Dispatch } from 'redux' -import { getCurrentMasterContractLastVersion } from 'src/logic/safe/utils/safeVersion' -import setLatestMasterContractVersion from 'src/logic/safe/store/actions/setLatestMasterContractVersion' - -const fetchLatestMasterContractVersion = - () => - async (dispatch: Dispatch): Promise => { - const latestVersion = await getCurrentMasterContractLastVersion() - - dispatch(setLatestMasterContractVersion(latestVersion)) - } - -export default fetchLatestMasterContractVersion diff --git a/src/logic/safe/store/actions/fetchTransactionDetails.ts b/src/logic/safe/store/actions/fetchTransactionDetails.ts index 0c71ab38d8..943b2225e7 100644 --- a/src/logic/safe/store/actions/fetchTransactionDetails.ts +++ b/src/logic/safe/store/actions/fetchTransactionDetails.ts @@ -3,46 +3,18 @@ import { createAction } from 'redux-actions' import { AddressEx, MultisigConfirmation, TransactionStatus } from '@gnosis.pm/safe-react-gateway-sdk' import { getInternalChainId } from 'src/config' import { currentChainId } from 'src/logic/config/store/selectors' +import { AppReduxState } from 'src/logic/safe/store' import { Dispatch } from 'src/logic/safe/store/actions/types' import { Transaction } from 'src/logic/safe/store/models/types/gateway.d' import { TransactionDetailsPayload } from 'src/logic/safe/store/reducer/gatewayTransactions' import { getTransactionByAttribute } from 'src/logic/safe/store/selectors/gatewayTransactions' -import { fetchSafeTransaction } from 'src/logic/safe/transactions/api/fetchSafeTransaction' import { extractSafeAddress } from 'src/routes/routes' import { getProposalDetail, getTxDetailById } from 'src/services' import { MESSAGES_CODE } from 'src/services/constant/message' -import { AppReduxState } from 'src/logic/safe/store' export const UPDATE_TRANSACTION_DETAILS = 'UPDATE_TRANSACTION_DETAILS' const updateTransactionDetails = createAction(UPDATE_TRANSACTION_DETAILS) -export const fetchTransactionDetails = - ({ transactionId }: { transactionId: Transaction['id'] }) => - async (dispatch: Dispatch, getState: () => AppReduxState): Promise => { - const transaction = getTransactionByAttribute(getState(), { - attributeValue: transactionId, - attributeName: 'id', - }) - const safeAddress = extractSafeAddress() - const chainId = currentChainId(getState()) - - if (transaction?.txDetails || !safeAddress) { - return - } - - try { - const transactionDetails = await fetchSafeTransaction(transactionId) - - dispatch(updateTransactionDetails({ chainId, transactionId, safeAddress, value: transactionDetails })) - } catch (error) { - console.error(`Failed to retrieve transaction ${transactionId} details`, error.message) - } - } - -type DetailedExecutionInfoExtended = { - gasPrice: string -} - export const fetchTransactionDetailsById = ({ transactionId, auraTxId }: { transactionId?: string; auraTxId?: string }) => async (dispatch: Dispatch, getState: () => AppReduxState): Promise => { diff --git a/src/logic/safe/store/actions/processTransaction.ts b/src/logic/safe/store/actions/processTransaction.ts deleted file mode 100644 index 980ee44f28..0000000000 --- a/src/logic/safe/store/actions/processTransaction.ts +++ /dev/null @@ -1,203 +0,0 @@ -import { List } from 'immutable' -import { AnyAction } from 'redux' -import { ThunkAction } from 'redux-thunk' - -import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts' -import { getNotificationsFromTxType, NOTIFICATIONS } from 'src/logic/notifications' -import { - checkIfOffChainSignatureIsPossible, - generateSignaturesFromTxConfirmations, - getPreValidatedSignatures, -} from 'src/logic/safe/safeTxSigner' -import { getApprovalTransaction, getExecutionTransaction, saveTxToHistory } from 'src/logic/safe/transactions' -import { tryOffChainSigning } from 'src/logic/safe/transactions/offchainSigner' -import * as aboutToExecuteTx from 'src/logic/safe/utils/aboutToExecuteTx' -import { currentChainId } from 'src/logic/config/store/selectors' -import { currentSafeCurrentVersion } from 'src/logic/safe/store/selectors' -import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions' -import { providerSelector } from 'src/logic/wallets/store/selectors' -import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar' -import closeSnackbarAction from 'src/logic/notifications/store/actions/closeSnackbar' -import { fetchSafe } from 'src/logic/safe/store/actions/fetchSafe' -import fetchTransactions from 'src/logic/safe/store/actions/transactions/fetchTransactions' -import { shouldExecuteTransaction } from 'src/logic/safe/store/actions/utils' -import { AppReduxState } from 'src/logic/safe/store' -import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters' -import { Dispatch, DispatchReturn } from './types' -import { PayableTx } from 'src/types/contracts/types' -import { updateTransactionStatus } from 'src/logic/safe/store/actions/updateTransactionStatus' -import { Confirmation } from 'src/logic/safe/store/models/types/confirmation' -import { Operation } from '@gnosis.pm/safe-react-gateway-sdk' -import { Errors, logError } from 'src/logic/exceptions/CodedException' -import { onboardUser } from 'src/components/ConnectButton' -import { getGasParam } from '../../transactions/gas' -import { getLastTransaction } from '../selectors/gatewayTransactions' -import { getRecommendedNonce } from '../../api/fetchSafeTxGasEstimation' -import { LocalTransactionStatus } from '../models/types/gateway.d' -import { isTxPendingError } from 'src/logic/wallets/getWeb3' - -interface ProcessTransactionArgs { - approveAndExecute: boolean - notifiedTransaction: string - safeAddress: string - tx: { - id: string - confirmations: List - origin: string // json.stringified url, name - to: string - value: string - data: string - operation: Operation - nonce: number - safeTxGas: string - safeTxHash: string - baseGas: string - gasPrice: string - gasToken: string - refundReceiver: string - } - userAddress: string - ethParameters?: Pick - thresholdReached: boolean -} - -type ProcessTransactionAction = ThunkAction, AppReduxState, DispatchReturn, AnyAction> - -export const processTransaction = - ({ - approveAndExecute, - notifiedTransaction, - safeAddress, - tx, - userAddress, - ethParameters, - thresholdReached, - }: ProcessTransactionArgs): ProcessTransactionAction => - async (dispatch: Dispatch, getState: () => AppReduxState): Promise => { - const ready = await onboardUser() - if (!ready) return - - const state = getState() - - const { account: from, hardwareWallet, smartContractWallet } = providerSelector(state) - const chainId = currentChainId(state) - const safeVersion = currentSafeCurrentVersion(state) as string - const safeInstance = getGnosisSafeInstanceAt(safeAddress, safeVersion) - - const lastTx = getLastTransaction(state) - let nonce: string - try { - nonce = (await getRecommendedNonce(safeAddress)).toString() - } catch (e) { - logError(Errors._616, e.message) - nonce = await safeInstance.methods.nonce().call() - } - const isExecution = approveAndExecute || (await shouldExecuteTransaction(safeInstance, nonce, lastTx)) - - const preApprovingOwner = approveAndExecute && !thresholdReached ? userAddress : undefined - let sigs = generateSignaturesFromTxConfirmations(tx.confirmations, preApprovingOwner) - - if (!sigs) { - sigs = getPreValidatedSignatures(from) - } - - const notificationsQueue = getNotificationsFromTxType(notifiedTransaction, tx.origin) - const beforeExecutionKey = dispatch(enqueueSnackbar(notificationsQueue.beforeExecution)) - - let txHash - let transaction - const txArgs = { - ...tx, // merge the previous tx with new data - safeInstance, - to: tx.to, - valueInWei: tx.value, - data: tx.data ?? EMPTY_DATA, - operation: tx.operation, - nonce: tx.nonce, - safeTxGas: tx.safeTxGas, - baseGas: tx.baseGas, - gasPrice: tx.gasPrice || '0', - gasToken: tx.gasToken, - refundReceiver: tx.refundReceiver, - sender: from, - sigs, - } - - try { - if (checkIfOffChainSignatureIsPossible(isExecution, smartContractWallet, safeVersion)) { - const signature = await tryOffChainSigning( - tx.safeTxHash, - { ...txArgs, safeAddress }, - hardwareWallet, - safeVersion, - ) - - if (signature) { - dispatch(closeSnackbarAction({ key: beforeExecutionKey })) - - await saveTxToHistory({ ...txArgs, signature }) - - dispatch(fetchTransactions(chainId, safeAddress)) - return - } - } - - transaction = isExecution ? getExecutionTransaction(txArgs) : getApprovalTransaction(safeInstance, tx.safeTxHash) - - const sendParams: PayableTx = { - from, - value: 0, - gas: ethParameters?.ethGasLimit, - [getGasParam()]: ethParameters?.ethGasPriceInGWei, - nonce: ethParameters?.ethNonce, - } - - await transaction - .send(sendParams) - .once('transactionHash', async (hash: string) => { - txHash = hash - dispatch(closeSnackbarAction({ key: beforeExecutionKey })) - - if (isExecution) { - dispatch(updateTransactionStatus({ safeTxHash: tx.safeTxHash, status: LocalTransactionStatus.PENDING })) - aboutToExecuteTx.setNonce(txArgs.nonce) - } - - if (!isExecution) { - try { - await saveTxToHistory({ ...txArgs }) - } catch (e) { - logError(Errors._804, e.message) - } - } - }) - .then(async (receipt) => { - dispatch(fetchTransactions(chainId, safeAddress)) - - if (isExecution) { - dispatch(fetchSafe(safeAddress)) - } - - return receipt.transactionHash - }) - } catch (err) { - logError(Errors._804, err.message) - - dispatch(closeSnackbarAction({ key: beforeExecutionKey })) - - if (isExecution) { - dispatch(updateTransactionStatus({ safeTxHash: tx.safeTxHash, status: LocalTransactionStatus.PENDING_FAILED })) - } - - const notification = isTxPendingError(err) - ? NOTIFICATIONS.TX_PENDING_MSG - : { - ...notificationsQueue.afterExecutionError, - message: `${notificationsQueue.afterExecutionError.message} - ${err.message}`, - } - - dispatch(enqueueSnackbar({ key: err.code, ...notification })) - } - - return txHash - } diff --git a/src/logic/safe/store/actions/setLastOpenedSafe.ts b/src/logic/safe/store/actions/setLastOpenedSafe.ts deleted file mode 100644 index de9bf7e9dd..0000000000 --- a/src/logic/safe/store/actions/setLastOpenedSafe.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { createAction } from 'redux-actions' - -export const SET_LAST_OPENED_SAFE = 'SET_LAST_OPENED_SAFE' - -export const setLastOpenedSafe = createAction(SET_LAST_OPENED_SAFE, (address: string) => ({ - address, -})) diff --git a/src/logic/safe/store/actions/transactions/fetchTransactions/loadGatewayTransactions.ts b/src/logic/safe/store/actions/transactions/fetchTransactions/loadGatewayTransactions.ts index 8f7536439d..d9dcde719e 100644 --- a/src/logic/safe/store/actions/transactions/fetchTransactions/loadGatewayTransactions.ts +++ b/src/logic/safe/store/actions/transactions/fetchTransactions/loadGatewayTransactions.ts @@ -1,14 +1,14 @@ -import { getTransactionHistory, getTransactionQueue, TransactionListItem } from '@gnosis.pm/safe-react-gateway-sdk' -import { getInternalChainId, _getChainId } from 'src/config' +import { getTransactionHistory, TransactionListItem } from '@gnosis.pm/safe-react-gateway-sdk' +import isEqual from 'lodash/isEqual' +import { _getChainId, getInternalChainId } from 'src/config' +import { CodedException, Errors } from 'src/logic/exceptions/CodedException' import { HistoryGatewayResponse, QueuedGatewayResponse } from 'src/logic/safe/store/models/types/gateway.d' -import { checksumAddress } from 'src/utils/checksumAddress' -import { Errors, CodedException } from 'src/logic/exceptions/CodedException' -import { GATEWAY_URL } from 'src/utils/constants' import { getAllTx } from 'src/services' -import { makeQueueTransactionsFromService, makeHistoryTransactionsFromService } from 'src/utils/transactionUtils' -import isEqual from 'lodash/isEqual' import { DEFAULT_PAGE_FIRST, DEFAULT_PAGE_SIZE, QUEUED_PAGE_SIZE } from 'src/services/constant/common' import { ITransactionListQuery } from 'src/types/transaction' +import { checksumAddress } from 'src/utils/checksumAddress' +import { GATEWAY_URL } from 'src/utils/constants' +import { makeHistoryTransactionsFromService, makeQueueTransactionsFromService } from 'src/utils/transactionUtils' /*************/ /* HISTORY */ @@ -47,25 +47,6 @@ export const loadPagedHistoryTransactions = async ( } } -export const loadHistoryTransactions = async (safeAddress: string): Promise => { - const chainId = _getChainId() - try { - const { results, next, previous } = await getTransactionHistory(GATEWAY_URL, chainId, checksumAddress(safeAddress)) - - if (!historyPointers[chainId]) { - historyPointers[chainId] = {} - } - - if (!historyPointers[chainId][safeAddress]) { - historyPointers[chainId][safeAddress] = { next, previous } - } - - return results - } catch (e) { - throw new CodedException(Errors._602, e.message) - } -} - export const loadHistoryTransactionsFromAuraApi = async ( safeAddress: string, ): Promise => { @@ -161,50 +142,6 @@ const queuedTransactions: { [chainId: string]: { [safeAddress: string]: { txs?: * If the fetch was success, updates the pointers. * @param {string} safeAddress */ -export const loadPagedQueuedTransactions = async ( - safeAddress: string, -): Promise<{ values: QueuedGatewayResponse['results']; next?: string } | undefined> => { - const chainId = _getChainId() - // if `queuedPointers[safeAddress] is `undefined` it means `loadHistoryTransactions` wasn't called - // if `queuedPointers[safeAddress].next is `null`, it means it reached the last page in gateway-client - if (!queuedPointers[safeAddress]?.next) { - throw new CodedException(Errors._608) - } - - try { - const { results, next, previous } = await getTransactionQueue( - GATEWAY_URL, - chainId, - checksumAddress(safeAddress), - queuedPointers[chainId][safeAddress].next, - ) - - queuedPointers[chainId][safeAddress] = { next, previous } - - return { values: results, next: queuedPointers[chainId][safeAddress].next } - } catch (e) { - throw new CodedException(Errors._603, e.message) - } -} - -export const loadQueuedTransactions = async (safeAddress: string): Promise => { - const chainId = _getChainId() - try { - const { results, next, previous } = await getTransactionQueue(GATEWAY_URL, chainId, checksumAddress(safeAddress)) - - if (!queuedPointers[chainId]) { - queuedPointers[chainId] = {} - } - - if (!queuedPointers[chainId][safeAddress] || queuedPointers[chainId][safeAddress].next === null) { - queuedPointers[chainId][safeAddress] = { next, previous } - } - - return results - } catch (e) { - throw new CodedException(Errors._603, e.message) - } -} export const loadQueuedTransactionsFromAuraApi = async ( safeAddress: string, diff --git a/src/logic/safe/store/actions/transactions/fetchTransactions/loadOutgoingTransactions.ts b/src/logic/safe/store/actions/transactions/fetchTransactions/loadOutgoingTransactions.ts deleted file mode 100644 index be67d7873b..0000000000 --- a/src/logic/safe/store/actions/transactions/fetchTransactions/loadOutgoingTransactions.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { DataDecoded } from 'src/logic/safe/store/models/types/transactions.d' - -export type ConfirmationServiceModel = { - confirmationType: string - owner: string - submissionDate: string - signature: string - signatureType: string - transactionHash: string -} - -export type TxServiceModel = { - baseGas: number - blockNumber?: number | null - confirmations: ConfirmationServiceModel[] - confirmationsRequired: number - data: string | null - dataDecoded?: DataDecoded - ethGasPrice: string - executionDate?: string | null - executor: string - fee: string - gasPrice: string - gasToken: string - gasUsed: number - isExecuted: boolean - isSuccessful: boolean - modified: string - nonce: number - operation: number - origin: string | null - refundReceiver: string - safe: string - safeTxGas: number - safeTxHash: string - signatures: string - submissionDate: string | null - to: string - transactionHash?: string | null - value: string -} diff --git a/src/logic/safe/store/actions/types.d.ts b/src/logic/safe/store/actions/types.d.ts index c91f633434..480c66e296 100644 --- a/src/logic/safe/store/actions/types.d.ts +++ b/src/logic/safe/store/actions/types.d.ts @@ -3,6 +3,6 @@ import { AnyAction } from 'redux' import { AppReduxState } from 'src/logic/safe/store' -export type DispatchReturn = string | undefined +type DispatchReturn = string | undefined export type Dispatch = ThunkDispatch diff --git a/src/logic/safe/store/index.ts b/src/logic/safe/store/index.ts index 24b396626b..1f4e75086e 100644 --- a/src/logic/safe/store/index.ts +++ b/src/logic/safe/store/index.ts @@ -1,7 +1,7 @@ -import { ValidatorStateType } from '../../validator/store/reducer/index' -import { applyMiddleware, CombinedState, combineReducers, compose, createStore, PreloadedState } from 'redux' -import { save, load, LoadOptions, RLSOptions } from 'redux-localstorage-simple' +import { applyMiddleware, CombinedState, combineReducers, compose, createStore } from 'redux' +import { load, LoadOptions, RLSOptions, save } from 'redux-localstorage-simple' import thunk from 'redux-thunk' +import { ValidatorStateType } from '../../validator/store/reducer/index' import { addressBookMiddleware } from 'src/logic/addressBook/store/middleware' import addressBookReducer, { ADDRESS_BOOK_REDUCER_ID } from 'src/logic/addressBook/store/reducer' @@ -13,54 +13,53 @@ import { } from 'src/logic/collectibles/store/reducer/collectibles' import cookiesReducer, { COOKIES_REDUCER_ID } from 'src/logic/cookies/store/reducer/cookies' import currentSessionReducer, { - CurrentSessionState, CURRENT_SESSION_REDUCER_ID, + CurrentSessionState, } from 'src/logic/currentSession/store/reducer/currentSession' import notificationsReducer, { NOTIFICATIONS_REDUCER_ID } from 'src/logic/notifications/store/reducer/notifications' +import notificationsMiddleware from 'src/logic/safe/store/middleware/notificationsMiddleware' +import { safeStorageMiddleware } from 'src/logic/safe/store/middleware/safeStorage' import gatewayTransactionsReducer, { - GatewayTransactionsState, GATEWAY_TRANSACTIONS_ID, + GatewayTransactionsState, } from 'src/logic/safe/store/reducer/gatewayTransactions' import localTransactionsReducer, { - LocalStatusesState, LOCAL_TRANSACTIONS_ID, + LocalStatusesState, } from 'src/logic/safe/store/reducer/localTransactions' -import tokensReducer, { TokenState, TOKEN_REDUCER_ID } from 'src/logic/tokens/store/reducer/tokens' -import providerWatcher from 'src/logic/wallets/store/middlewares/providerWatcher' -import providerReducer, { ProviderState, PROVIDER_REDUCER_ID } from 'src/logic/wallets/store/reducer/provider' -import notificationsMiddleware from 'src/logic/safe/store/middleware/notificationsMiddleware' -import { safeStorageMiddleware } from 'src/logic/safe/store/middleware/safeStorage' import safeReducer, { SAFE_REDUCER_ID } from 'src/logic/safe/store/reducer/safe' +import tokensReducer, { TOKEN_REDUCER_ID, TokenState } from 'src/logic/tokens/store/reducer/tokens' +import providerReducer, { PROVIDER_REDUCER_ID, ProviderState } from 'src/logic/wallets/store/reducer/provider' import currencyValuesReducer, { - CurrencyValuesState, CURRENCY_REDUCER_ID, + CurrencyValuesState, initialCurrencyState, } from 'src/logic/currencyValues/store/reducer/currencyValues' -import termReducer, { TermState, TermInitialState, TERM_ID } from 'src/logic/checkTerm/store/reducer/term' +import termReducer, { TERM_ID, TermState } from 'src/logic/checkTerm/store/reducer/term' -import configReducer, { CONFIG_REDUCER_ID, initialConfigState } from 'src/logic/config/store/reducer' -import { configMiddleware } from 'src/logic/config/store/middleware' import { AddressBookState } from 'src/logic/addressBook/model/addressBook' import appearanceReducer, { APPEARANCE_REDUCER_ID, - initialAppearanceState, AppearanceState, + initialAppearanceState, } from 'src/logic/appearance/reducer/appearance' import { NFTAssets, NFTTokens } from 'src/logic/collectibles/sources/collectibles' -import { SafeReducerMap } from 'src/logic/safe/store/reducer/types/safe' -import { LS_NAMESPACE, LS_SEPARATOR } from 'src/utils/constants' +import { configMiddleware } from 'src/logic/config/store/middleware' +import configReducer, { CONFIG_REDUCER_ID, initialConfigState } from 'src/logic/config/store/reducer' import { ConfigState } from 'src/logic/config/store/reducer/reducer' +import delegationReducer, { DELEGATION_REDUCER_ID, DelegationStateType } from 'src/logic/delegation/store/reducer' +import { PROPOSALS_REDUCER_ID, proposalsReducer } from 'src/logic/proposal/store/reducer/proposals' import { localTransactionsMiddleware } from 'src/logic/safe/store/middleware/localTransactionsMiddleware' -import { proposalsReducer, PROPOSALS_REDUCER_ID } from 'src/logic/proposal/store/reducer/proposals' -import { IProposal } from 'src/types/proposal' +import { SafeReducerMap } from 'src/logic/safe/store/reducer/types/safe' import validatorReducer, { VALIDATOR_REDUCER_ID } from 'src/logic/validator/store/reducer' -import delegationReducer, { DelegationStateType, DELEGATION_REDUCER_ID } from 'src/logic/delegation/store/reducer' +import { IProposal } from 'src/types/proposal' +import { LS_NAMESPACE, LS_SEPARATOR } from 'src/utils/constants' const CURRENCY_KEY = `${CURRENCY_REDUCER_ID}.selectedCurrency` -export const LS_CONFIG: RLSOptions | LoadOptions = { +const LS_CONFIG: RLSOptions | LoadOptions = { states: [ADDRESS_BOOK_REDUCER_ID, CURRENCY_KEY, APPEARANCE_REDUCER_ID, CONFIG_REDUCER_ID, TERM_ID], namespace: LS_NAMESPACE, namespaceSeparator: LS_SEPARATOR, @@ -137,6 +136,3 @@ export type AppReduxState = CombinedState<{ }> export const store: any = createStore(rootReducer, load(LS_CONFIG), enhancer) - -export const createPreloadedStore = (localState = {} as PreloadedState): typeof store => - createStore(rootReducer, localState, enhancer) diff --git a/src/logic/safe/store/models/confirmation.ts b/src/logic/safe/store/models/confirmation.ts deleted file mode 100644 index 81b63552fb..0000000000 --- a/src/logic/safe/store/models/confirmation.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Record } from 'immutable' -import { ConfirmationProps } from './types/confirmation' - -export const makeConfirmation = Record({ - owner: '', - type: 'initialised', - hash: '', - signature: null, -}) diff --git a/src/logic/safe/store/models/safe.ts b/src/logic/safe/store/models/safe.ts index 604683757b..c6f242a411 100644 --- a/src/logic/safe/store/models/safe.ts +++ b/src/logic/safe/store/models/safe.ts @@ -4,7 +4,7 @@ import { ChainId } from 'src/config/chain.d' import { BalanceRecord } from 'src/logic/tokens/store/actions/fetchSafeTokens' -export type SafeOwner = string +type SafeOwner = string export type ModulePair = [ // previous module diff --git a/src/logic/safe/store/models/types/gateway.d.ts b/src/logic/safe/store/models/types/gateway.d.ts index 6f792cce2e..037cd274ea 100644 --- a/src/logic/safe/store/models/types/gateway.d.ts +++ b/src/logic/safe/store/models/types/gateway.d.ts @@ -44,7 +44,7 @@ export type Transaction = TransactionSummary & { confirmationsRequired?: number } -export type ExpandedTxDetails = GWTransactionDetails +type ExpandedTxDetails = GWTransactionDetails type StoreStructure = { queued: { diff --git a/src/logic/safe/store/models/types/transaction.ts b/src/logic/safe/store/models/types/transaction.ts index 264a8322bd..c3856077e8 100644 --- a/src/logic/safe/store/models/types/transaction.ts +++ b/src/logic/safe/store/models/types/transaction.ts @@ -1,12 +1,6 @@ import { Operation } from '@gnosis.pm/safe-react-gateway-sdk' import { GnosisSafe } from 'src/types/contracts/gnosis_safe.d' -export enum PendingActionType { - CONFIRM = 'confirm', - REJECT = 'reject', -} -export type PendingActionValues = PendingActionType[keyof PendingActionType] - export type TxArgs = { baseGas: string data: string diff --git a/src/logic/safe/store/models/types/transactions.d.ts b/src/logic/safe/store/models/types/transactions.d.ts index ebddc5d8a0..41b32ccca8 100644 --- a/src/logic/safe/store/models/types/transactions.d.ts +++ b/src/logic/safe/store/models/types/transactions.d.ts @@ -1,17 +1,18 @@ -export enum Operation { +enum Operation { CALL, DELEGATE_CALL, CREATE, } // types comes from: https://github.com/gnosis/safe-client-gateway/blob/752e76b6d1d475791dbd7917b174bb41d2d9d8be/src/utils.rs -export enum TransferMethods { + +enum TransferMethods { TRANSFER = 'transfer', TRANSFER_FROM = 'transferFrom', SAFE_TRANSFER_FROM = 'safeTransferFrom', } -export enum SettingsChangeMethods { +enum SettingsChangeMethods { SETUP = 'setup', SET_FALLBACK_HANDLER = 'setFallbackHandler', ADD_OWNER_WITH_THRESHOLD = 'addOwnerWithThreshold', @@ -28,9 +29,9 @@ export enum SettingsChangeMethods { } // note: this extends SAFE_METHODS_NAMES in /logic/contracts/methodIds.ts, we need to figure out which one we are going to use -export type DataDecodedMethod = TransferMethods | SettingsChangeMethods | string +type DataDecodedMethod = TransferMethods | SettingsChangeMethods | string -export interface ValueDecoded { +interface ValueDecoded { operation: Operation to: string value: number @@ -38,7 +39,7 @@ export interface ValueDecoded { dataDecoded: DataDecoded } -export interface SingleTransactionMethodParameter { +interface SingleTransactionMethodParameter { name: string type: string value: string @@ -48,7 +49,7 @@ export interface MultiSendMethodParameter extends SingleTransactionMethodParamet valueDecoded: ValueDecoded[] } -export type Parameter = MultiSendMethodParameter | SingleTransactionMethodParameter +type Parameter = MultiSendMethodParameter | SingleTransactionMethodParameter export interface DataDecoded { method: DataDecodedMethod @@ -93,7 +94,7 @@ export interface DataDecoded { // { name: "getTransactionHash", id: "0xd8d11f78" } // ] -export const SAFE_METHODS_NAMES = { +const SAFE_METHODS_NAMES = { ADD_OWNER_WITH_THRESHOLD: 'addOwnerWithThreshold', CHANGE_THRESHOLD: 'changeThreshold', REMOVE_OWNER: 'removeOwner', @@ -102,7 +103,7 @@ export const SAFE_METHODS_NAMES = { DISABLE_MODULE: 'disableModule', } as const -export const SAFE_METHOD_ID_TO_NAME = { +const SAFE_METHOD_ID_TO_NAME = { '0xe318b52b': SAFE_METHODS_NAMES.SWAP_OWNER, '0x0d582f13': SAFE_METHODS_NAMES.ADD_OWNER_WITH_THRESHOLD, '0xf8dc5dd9': SAFE_METHODS_NAMES.REMOVE_OWNER, @@ -111,29 +112,29 @@ export const SAFE_METHOD_ID_TO_NAME = { '0xe009cfde': SAFE_METHODS_NAMES.DISABLE_MODULE, } as const -export const SPENDING_LIMIT_METHODS_NAMES = { +const SPENDING_LIMIT_METHODS_NAMES = { ADD_DELEGATE: 'addDelegate', SET_ALLOWANCE: 'setAllowance', EXECUTE_ALLOWANCE_TRANSFER: 'executeAllowanceTransfer', DELETE_ALLOWANCE: 'deleteAllowance', } as const -export const SPENDING_LIMIT_METHOD_ID_TO_NAME = { +const SPENDING_LIMIT_METHOD_ID_TO_NAME = { '0xe71bdf41': SPENDING_LIMIT_METHODS_NAMES.ADD_DELEGATE, '0xbeaeb388': SPENDING_LIMIT_METHODS_NAMES.SET_ALLOWANCE, '0x4515641a': SPENDING_LIMIT_METHODS_NAMES.EXECUTE_ALLOWANCE_TRANSFER, '0x885133e3': SPENDING_LIMIT_METHODS_NAMES.DELETE_ALLOWANCE, } as const -export type SafeMethods = typeof SAFE_METHODS_NAMES[keyof typeof SAFE_METHODS_NAMES] +type SafeMethods = typeof SAFE_METHODS_NAMES[keyof typeof SAFE_METHODS_NAMES] -export const TOKEN_TRANSFER_METHODS_NAMES = { +const TOKEN_TRANSFER_METHODS_NAMES = { TRANSFER: 'transfer', TRANSFER_FROM: 'transferFrom', SAFE_TRANSFER_FROM: 'safeTransferFrom', } as const -export const TOKEN_TRANSFER_METHOD_ID_TO_NAME = { +const TOKEN_TRANSFER_METHOD_ID_TO_NAME = { '0xa9059cbb': TOKEN_TRANSFER_METHODS_NAMES.TRANSFER, '0x23b872dd': TOKEN_TRANSFER_METHODS_NAMES.TRANSFER_FROM, '0x42842e0e': TOKEN_TRANSFER_METHODS_NAMES.SAFE_TRANSFER_FROM, @@ -141,12 +142,12 @@ export const TOKEN_TRANSFER_METHOD_ID_TO_NAME = { type TokenMethods = typeof TOKEN_TRANSFER_METHODS_NAMES[keyof typeof TOKEN_TRANSFER_METHODS_NAMES] -export type SafeDecodedParams = { +type SafeDecodedParams = { [key in SafeMethods]?: Record } -export type TokenDecodedParams = { +type TokenDecodedParams = { [key in TokenMethods]?: Record } -export type DecodedParams = SafeDecodedParams | TokenDecodedParams | null +type DecodedParams = SafeDecodedParams | TokenDecodedParams | null diff --git a/src/logic/safe/store/reducer/gatewayTransactions.ts b/src/logic/safe/store/reducer/gatewayTransactions.ts index ccb9406758..fc96599a26 100644 --- a/src/logic/safe/store/reducer/gatewayTransactions.ts +++ b/src/logic/safe/store/reducer/gatewayTransactions.ts @@ -44,7 +44,7 @@ export type TransactionDetailsPayload = { type Payload = HistoryPayload | QueuedPayload | TransactionDetailsPayload -export const gatewayTransactionsReducer = handleActions( +const gatewayTransactionsReducer = handleActions( { [ADD_HISTORY_TRANSACTIONS]: (state, action: Action) => { const { chainId, safeAddress, values, isTail = false } = action.payload diff --git a/src/logic/safe/store/reducer/localTransactions.ts b/src/logic/safe/store/reducer/localTransactions.ts index 5e9f0ae364..0761006c95 100644 --- a/src/logic/safe/store/reducer/localTransactions.ts +++ b/src/logic/safe/store/reducer/localTransactions.ts @@ -15,7 +15,7 @@ export type LocalStatusesState = Record( +const localTransactionsReducer = handleActions( { [UPDATE_TRANSACTION_STATUS]: (state, action: Action) => { const { safeTxHash, status } = action.payload diff --git a/src/logic/safe/store/selectors/gatewayTransactions.ts b/src/logic/safe/store/selectors/gatewayTransactions.ts index adad9205fd..8498ef7bb9 100644 --- a/src/logic/safe/store/selectors/gatewayTransactions.ts +++ b/src/logic/safe/store/selectors/gatewayTransactions.ts @@ -2,6 +2,8 @@ import flatten from 'lodash/flatten' import get from 'lodash/get' import { createSelector } from 'reselect' +import { currentChainId } from 'src/logic/config/store/selectors' +import { AppReduxState } from 'src/logic/safe/store' import { isMultisigExecutionInfo, StoreStructure, @@ -9,12 +11,10 @@ import { TxLocation, } from 'src/logic/safe/store/models/types/gateway.d' import { GATEWAY_TRANSACTIONS_ID } from 'src/logic/safe/store/reducer/gatewayTransactions' -import { currentChainId } from 'src/logic/config/store/selectors' import { createHashBasedSelector } from 'src/logic/safe/store/selectors/utils' -import { AppReduxState } from 'src/logic/safe/store' import { extractSafeAddress } from 'src/routes/routes' -export const gatewayTransactions = (state: AppReduxState): AppReduxState['gatewayTransactions'] => { +const gatewayTransactions = (state: AppReduxState): AppReduxState['gatewayTransactions'] => { return state[GATEWAY_TRANSACTIONS_ID] } @@ -27,7 +27,7 @@ export const historyTransactions = createHashBasedSelector( }, ) -export const pendingTransactions = createSelector( +const pendingTransactions = createSelector( gatewayTransactions, currentChainId, (gatewayTransactions, chainId): StoreStructure['queued'] | undefined => { @@ -59,7 +59,7 @@ export const txTransactions = createSelector( const txLocations: TxLocation[] = ['queued.txs', 'history'] -export const getTransactionWithLocationByAttribute = createSelector( +const getTransactionWithLocationByAttribute = createSelector( gatewayTransactions, currentChainId, extractSafeAddress, @@ -102,41 +102,6 @@ export const getTransactionByAttribute = createSelector( }, ) -export const getTransactionsByNonce = createSelector( - gatewayTransactions, - currentChainId, - extractSafeAddress, - (_: AppReduxState, nonce: number) => nonce, - (gatewayTransactions, chainId, safeAddress, nonce): Transaction[] => { - let txsByNonce: Transaction[] = [] - - for (const txLocation of txLocations) { - const storedTxs: StoreStructure['history'] | StoreStructure['queued']['txs'] | undefined = get( - gatewayTransactions?.[chainId]?.[safeAddress], - txLocation, - ) - - if (!storedTxs) { - continue - } - - for (const txs of Object.values(storedTxs)) { - const txFoundByNonce = txs.filter( - (tx) => isMultisigExecutionInfo(tx?.executionInfo) && tx.executionInfo?.nonce === nonce, - ) - - if (!txFoundByNonce.length) { - continue - } - - txsByNonce = [...txsByNonce, ...txFoundByNonce] - } - } - - return txsByNonce - }, -) - export const getLastTransaction = createSelector( nextTransactions, queuedTransactions, diff --git a/src/logic/safe/store/selectors/index.ts b/src/logic/safe/store/selectors/index.ts index b90a1ea705..f515a61811 100644 --- a/src/logic/safe/store/selectors/index.ts +++ b/src/logic/safe/store/selectors/index.ts @@ -3,11 +3,11 @@ import { createSelector } from 'reselect' import { AddressBookEntry, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook' import { currentNetworkAddressBookAsMap } from 'src/logic/addressBook/store/selectors' import { currentChainId } from 'src/logic/config/store/selectors' +import { AppReduxState } from 'src/logic/safe/store' import makeSafe, { SafeRecord, SafeRecordProps } from 'src/logic/safe/store/models/safe' import { SAFE_REDUCER_ID } from 'src/logic/safe/store/reducer/safe' import { SafesMap } from 'src/logic/safe/store/reducer/types/safe' import { extractSafeAddress } from 'src/routes/routes' -import { AppReduxState } from 'src/logic/safe/store' import { Overwrite } from 'src/types/helpers' const safesState = (state: AppReduxState) => state[SAFE_REDUCER_ID] @@ -16,10 +16,6 @@ export const safesAsMap = (state: AppReduxState): SafesMap => safesState(state). export const safesAsList = createSelector(safesAsMap, (safes): List => safes.toList()) -export const latestMasterContractVersion = createSelector(safesState, (safeState) => - safeState.get('latestMasterContractVersion'), -) - export const currentSafe = createSelector([safesAsMap], (safes: SafesMap) => { const address = extractSafeAddress() return safes.get(address, baseSafe(address)) @@ -27,7 +23,7 @@ export const currentSafe = createSelector([safesAsMap], (safes: SafesMap) => { const baseSafe = (address = '') => makeSafe({ address }) -export const safeFieldSelector = +const safeFieldSelector = (field: K) => (safe: SafeRecord): SafeRecordProps[K] => safe.get(field, baseSafe().get(field)) @@ -36,30 +32,22 @@ export const currentSafeEthBalance = createSelector(currentSafe, safeFieldSelect export const currentSafeBalances = createSelector(currentSafe, safeFieldSelector('balances')) -export const currentSafeNeedsUpdate = createSelector(currentSafe, safeFieldSelector('needsUpdate')) - export const currentSafeCurrentVersion = createSelector(currentSafe, safeFieldSelector('currentVersion')) export const currentSafeThreshold = createSelector(currentSafe, safeFieldSelector('threshold')) -export const currentSafeNonce = createSelector(currentSafe, safeFieldSelector('nonce')) - export const currentSafeOwners = createSelector(currentSafe, safeFieldSelector('owners')) -export const currentSafeModules = createSelector(currentSafe, safeFieldSelector('modules')) - export const currentSafeFeaturesEnabled = createSelector(currentSafe, safeFieldSelector('featuresEnabled')) export const currentSafeSpendingLimits = createSelector(currentSafe, safeFieldSelector('spendingLimits')) -export const currentSafeTotalFiatBalance = createSelector(currentSafe, safeFieldSelector('totalFiatBalance')) - /*************************/ /* With AddressBook Data */ /*************************/ const baseSafeWithName = (address = '') => ({ ...baseSafe(address).toJS(), name: '' }) -export type SafeRecordWithNames = Overwrite & { name: string } +type SafeRecordWithNames = Overwrite & { name: string } export const safesWithNamesAsList = createSelector( [safesAsList, currentNetworkAddressBookAsMap, currentChainId], diff --git a/src/logic/safe/store/selectors/utils.ts b/src/logic/safe/store/selectors/utils.ts index 135c3bb301..464886ac17 100644 --- a/src/logic/safe/store/selectors/utils.ts +++ b/src/logic/safe/store/selectors/utils.ts @@ -1,12 +1,9 @@ -import hash from 'object-hash' -import isEqual from 'lodash/isEqual' import memoize from 'lodash/memoize' -import { createSelectorCreator, defaultMemoize } from 'reselect' +import hash from 'object-hash' +import { createSelectorCreator } from 'reselect' import { AppReduxState } from 'src/logic/safe/store' -export const createIsEqualSelector = createSelectorCreator(defaultMemoize, isEqual) - const hashFn = (gatewayTransactions: AppReduxState['gatewayTransactions'], safeAddress: string): string => hash(gatewayTransactions[safeAddress] ?? {}) diff --git a/src/logic/safe/transactions/gas.ts b/src/logic/safe/transactions/gas.ts index 8697f422da..7e64dbf0d2 100644 --- a/src/logic/safe/transactions/gas.ts +++ b/src/logic/safe/transactions/gas.ts @@ -198,7 +198,7 @@ type TransactionApprovalEstimationProps = { isOffChainSignature: boolean } -export const estimateGasForTransactionApproval = async ({ +const estimateGasForTransactionApproval = async ({ safeAddress, safeVersion, txRecipient, diff --git a/src/logic/safe/utils/guardManager.ts b/src/logic/safe/utils/guardManager.ts index a54ebcbbb6..9ee01e4b00 100644 --- a/src/logic/safe/utils/guardManager.ts +++ b/src/logic/safe/utils/guardManager.ts @@ -1,7 +1,7 @@ import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts' import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses' -export const getSetGuardTxData = (guardAddress: string, safeAddress: string, safeVersion: string): string => { +const getSetGuardTxData = (guardAddress: string, safeAddress: string, safeVersion: string): string => { const safeInstance = getGnosisSafeInstanceAt(safeAddress, safeVersion) return safeInstance.methods.setGuard(guardAddress).encodeABI() diff --git a/src/logic/tokens/store/model/token.ts b/src/logic/tokens/store/model/token.ts index 0e0497f26d..fe4e781bd9 100644 --- a/src/logic/tokens/store/model/token.ts +++ b/src/logic/tokens/store/model/token.ts @@ -3,7 +3,7 @@ import { Record, RecordOf } from 'immutable' import { BalanceRecord } from 'src/logic/tokens/store/actions/fetchSafeTokens' -export type TokenProps = { +type TokenProps = { address: string name: string symbol: string diff --git a/src/logic/tokens/store/selectors/index.ts b/src/logic/tokens/store/selectors/index.ts index e266f2b977..76286f3cba 100644 --- a/src/logic/tokens/store/selectors/index.ts +++ b/src/logic/tokens/store/selectors/index.ts @@ -1,12 +1,4 @@ -import { createSelector } from 'reselect' - -import { TOKEN_REDUCER_ID, TokenState } from 'src/logic/tokens/store/reducer/tokens' import { AppReduxState } from 'src/logic/safe/store' +import { TOKEN_REDUCER_ID, TokenState } from 'src/logic/tokens/store/reducer/tokens' export const tokensSelector = (state: AppReduxState): TokenState => state[TOKEN_REDUCER_ID] - -export const tokenListSelector = createSelector(tokensSelector, (tokens) => tokens.toList()) - -export const orderedTokenListSelector = createSelector(tokenListSelector, (tokens) => - tokens.sortBy((token) => token.get('symbol')), -) diff --git a/src/logic/validator/store/actions/index.ts b/src/logic/validator/store/actions/index.ts index e704131d3b..38bc120dc6 100644 --- a/src/logic/validator/store/actions/index.ts +++ b/src/logic/validator/store/actions/index.ts @@ -28,4 +28,4 @@ export const fetchAllValidator = } return Promise.resolve() } -export const setAllValidator = createAction(FETCH_ALL_VALIDATORS) +const setAllValidator = createAction(FETCH_ALL_VALIDATORS) diff --git a/src/logic/validator/store/reducer/index.ts b/src/logic/validator/store/reducer/index.ts index 0b33aa4598..29e21150cc 100644 --- a/src/logic/validator/store/reducer/index.ts +++ b/src/logic/validator/store/reducer/index.ts @@ -14,7 +14,7 @@ export type ValidatorStateType = { allValidators: ValidatorType[] } -export const initialState = { +const initialState = { allValidators: [], } diff --git a/src/logic/wallets/ethAddresses.ts b/src/logic/wallets/ethAddresses.ts index 80b935d59f..4c1f0847fa 100644 --- a/src/logic/wallets/ethAddresses.ts +++ b/src/logic/wallets/ethAddresses.ts @@ -1,18 +1,12 @@ import { List } from 'immutable' import { SafeRecord } from 'src/logic/safe/store/models/safe' import { sameString } from 'src/utils/strings' -import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions' export const ZERO_ADDRESS = '0000000000000000000000000000000000000000' export const sameAddress = (firstAddress: string | undefined, secondAddress: string | undefined): boolean => { return sameString(firstAddress, secondAddress) } -export const isEmptyAddress = (address: string | undefined): boolean => { - if (!address) return true - return sameAddress(address, EMPTY_DATA) || sameAddress(address, ZERO_ADDRESS) -} - export const shortVersionOf = (value: string, cut: number): string => { if (!value) { return 'Unknown' diff --git a/src/logic/wallets/onboard.ts b/src/logic/wallets/onboard.ts index e4d49ef62a..6e25e81f6b 100644 --- a/src/logic/wallets/onboard.ts +++ b/src/logic/wallets/onboard.ts @@ -63,7 +63,7 @@ const getOnboardConfiguration = () => { } let currentOnboardInstance: API -export const onboard = (): API => { +const onboard = (): API => { const chainId = _getChainId() if (!currentOnboardInstance || currentOnboardInstance.getState().appNetworkId.toString() !== chainId) { currentOnboardInstance = Onboard(getOnboardConfiguration()) diff --git a/src/logic/wallets/utils/network.ts b/src/logic/wallets/utils/network.ts index 98a602f269..f7f8336313 100644 --- a/src/logic/wallets/utils/network.ts +++ b/src/logic/wallets/utils/network.ts @@ -2,9 +2,9 @@ import { Wallet } from 'bnc-onboard/dist/src/interfaces' import onboard from 'src/logic/wallets/onboard' import { numberToHex } from 'web3-utils' -import { getChainInfo, getExplorerUrl, getPublicRpcUrl, _getChainId } from 'src/config' +import { _getChainId, getChainInfo, getExplorerUrl, getPublicRpcUrl } from 'src/config' import { ChainId } from 'src/config/chain.d' -import { Errors, CodedException } from 'src/logic/exceptions/CodedException' +import { CodedException, Errors } from 'src/logic/exceptions/CodedException' const WALLET_ERRORS = { UNRECOGNIZED_CHAIN: 4902, @@ -82,13 +82,3 @@ export const shouldSwitchNetwork = (wallet = onboard().getState()?.wallet): bool return currentNetwork ? desiredNetwork !== currentNetwork.toString() : false } -export const switchWalletChain = async (): Promise => { - const { wallet } = onboard().getState() - try { - await switchNetwork(wallet, _getChainId()) - } catch (e) { - e.log() - // Fallback to the onboard popup if switching isn't supported - await onboard().walletCheck() - } -} diff --git a/src/pages/AddressBook/CreateEditEntryModal/index.tsx b/src/pages/AddressBook/CreateEditEntryModal/index.tsx index b7959b89cc..e4cac834d3 100644 --- a/src/pages/AddressBook/CreateEditEntryModal/index.tsx +++ b/src/pages/AddressBook/CreateEditEntryModal/index.tsx @@ -17,9 +17,9 @@ import { currentNetworkAddressBookAddresses } from 'src/logic/addressBook/store/ import { AddressBookEntry } from 'src/logic/addressBook/model/addressBook' import { Entry } from 'src/pages/AddressBook' -export const CREATE_ENTRY_INPUT_NAME_ID = 'create-entry-input-name' -export const CREATE_ENTRY_INPUT_ADDRESS_ID = 'create-entry-input-address' -export const SAVE_NEW_ENTRY_BTN_ID = 'save-new-entry-btn-id' +const CREATE_ENTRY_INPUT_NAME_ID = 'create-entry-input-name' +const CREATE_ENTRY_INPUT_ADDRESS_ID = 'create-entry-input-address' +const SAVE_NEW_ENTRY_BTN_ID = 'save-new-entry-btn-id' const formMutators = { setOwnerAddress: (args, state, utils) => { diff --git a/src/pages/AddressBook/DeleteEntryModal/index.tsx b/src/pages/AddressBook/DeleteEntryModal/index.tsx index d69c0849b5..46879bd105 100644 --- a/src/pages/AddressBook/DeleteEntryModal/index.tsx +++ b/src/pages/AddressBook/DeleteEntryModal/index.tsx @@ -5,7 +5,7 @@ import { Modal } from 'src/components/Modal' import GnoForm from 'src/components/forms/GnoForm' import { Entry } from 'src/pages/AddressBook' -export const DELETE_ENTRY_BTN_ID = 'delete-entry-btn-id' +const DELETE_ENTRY_BTN_ID = 'delete-entry-btn-id' interface DeleteEntryModalProps { deleteEntryModalHandler: () => void diff --git a/src/pages/AddressBook/EllipsisTransactionDetails/index.tsx b/src/pages/AddressBook/EllipsisTransactionDetails/index.tsx deleted file mode 100644 index e0670500a0..0000000000 --- a/src/pages/AddressBook/EllipsisTransactionDetails/index.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { useState } from 'react' -import { ClickAwayListener, createStyles, Divider } from '@material-ui/core' -import Menu from '@material-ui/core/Menu' -import MenuItem from '@material-ui/core/MenuItem' -import { makeStyles } from '@material-ui/core/styles' -import MoreHorizIcon from '@material-ui/icons/MoreHoriz' -import { useSelector } from 'react-redux' - -import { sameString } from 'src/utils/strings' -import { ADDRESS_BOOK_DEFAULT_NAME } from 'src/logic/addressBook/model/addressBook' -import { addressBookEntryName } from 'src/logic/addressBook/store/selectors' -import { xs } from 'src/theme/variables' -import { grantedSelector } from 'src/utils/safeUtils/selector' -import { SAFE_ROUTES, history, extractSafeAddress, generateSafeRoute } from 'src/routes/routes' -import { getShortName } from 'src/config' - -const useStyles = makeStyles( - createStyles({ - container: { - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - cursor: 'pointer', - margin: `0 ${xs}`, - borderRadius: '50%', - transition: 'background-color .2s ease-in-out', - '&:hover': { - backgroundColor: '#F0EFEE', - }, - outline: 'none', - }, - increasedPopperZindex: { - zIndex: 2001, - }, - }), -) - -type EllipsisTransactionDetailsProps = { - address: string - sendModalOpenHandler?: () => void -} - -export const EllipsisTransactionDetails = ({ - address, - sendModalOpenHandler, -}: EllipsisTransactionDetailsProps): React.ReactElement => { - const classes = useStyles() - const [anchorEl, setAnchorEl] = useState(null) - - const isOwnerConnected = useSelector(grantedSelector) - - const recipientName = useSelector((state) => addressBookEntryName(state, { address })) - // We have to check that the name returned is not UNKNOWN - const isStoredInAddressBook = !sameString(recipientName, ADDRESS_BOOK_DEFAULT_NAME) - - const handleClick = (event) => setAnchorEl(event.currentTarget) - - const closeMenuHandler = () => setAnchorEl(null) - - const addOrEditEntryHandler = () => { - history.push({ - pathname: generateSafeRoute(SAFE_ROUTES.ADDRESS_BOOK, { - shortName: getShortName(), - safeAddress: extractSafeAddress(), - }), - search: `?entryAddress=${address}`, - }) - closeMenuHandler() - } - - return ( - -
- - - {sendModalOpenHandler - ? [ - - Send Again - , - , - ] - : null} - {isStoredInAddressBook ? ( - Edit Address book Entry - ) : ( - Add to address book - )} - -
-
- ) -} diff --git a/src/pages/AddressBook/columns.ts b/src/pages/AddressBook/columns.ts index 79ddf5317b..e539c2abbd 100644 --- a/src/pages/AddressBook/columns.ts +++ b/src/pages/AddressBook/columns.ts @@ -2,13 +2,9 @@ import { List } from 'immutable' import { TableCellProps } from '@material-ui/core/TableCell/TableCell' export const ADDRESS_BOOK_ROW_ID = 'address-book-row' -export const TX_TABLE_ADDRESS_BOOK_ID = 'idAddressBook' export const AB_NAME_ID = 'name' export const AB_ADDRESS_ID = 'address' -export const AB_ADDRESS_ACTIONS_ID = 'actions' -export const EDIT_ENTRY_BUTTON = 'edit-entry-btn' -export const REMOVE_ENTRY_BUTTON = 'remove-entry-btn' -export const SEND_ENTRY_BUTTON = 'send-entry-btn' +const AB_ADDRESS_ACTIONS_ID = 'actions' type AddressBookColumn = { id: string diff --git a/src/pages/AddressBook/style.ts b/src/pages/AddressBook/style.ts index 0df7ecbe89..b056be6be2 100644 --- a/src/pages/AddressBook/style.ts +++ b/src/pages/AddressBook/style.ts @@ -67,17 +67,6 @@ export const styles = createStyles({ } }) -const StyledButton = styled(Button)` - &&.MuiButton-root { - margin: 4px 12px 4px 0px; - padding: 0 12px; - min-width: auto; - } - - svg { - margin: 0 6px 0 0; - } -` const StyledButtonLink = styled(ButtonLink)` text-decoration: none !important; p { @@ -87,4 +76,4 @@ const StyledButtonLink = styled(ButtonLink)` fill: #5ee6d0 !important; } ` -export { StyledButton, StyledButtonLink } +export { StyledButtonLink } diff --git a/src/pages/Staking/constant.tsx b/src/pages/Staking/constant.tsx deleted file mode 100644 index b0d1f902d5..0000000000 --- a/src/pages/Staking/constant.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export const getDisplayAddress = (address) => { - const result = `${address?.substring(0, 5)}...${address?.substring( - address ? address?.length - 5 : 0, - address ? address?.length : 1, - )}` - return result -} diff --git a/src/pages/Transactions/styled.tsx b/src/pages/Transactions/styled.tsx index b0ef85d6f4..20b50f5de0 100644 --- a/src/pages/Transactions/styled.tsx +++ b/src/pages/Transactions/styled.tsx @@ -1,4 +1,4 @@ -import { Text, Accordion, AccordionDetails, AccordionSummary, EthHashInfo } from '@aura/safe-react-components' +import { Accordion, AccordionSummary, EthHashInfo } from '@aura/safe-react-components' import styled, { css } from 'styled-components' export const Wrapper = styled.div` @@ -17,11 +17,6 @@ export const Wrapper = styled.div` } ` -export const StyledText = styled.span` - color: #98989b; - font-size: 14px; -` - export const ContentWrapper = styled.div` display: flex; flex-direction: column; @@ -30,10 +25,6 @@ export const ContentWrapper = styled.div` color: white; ` -export const ColumnDisplayAccordionDetails = styled(AccordionDetails)` - flex-flow: column; -` - export const AccordionWrapper = styled.div<{ hasSameSeqTxAfter?: boolean; hasSameSeqTxBefore?: boolean }>` background: #24262e; border: 1px solid #363843; @@ -85,40 +76,6 @@ export const NoPaddingAccordion = styled(Accordion)` } ` -export const ActionAccordion = styled(Accordion)` - &.MuiAccordion-root { - background-color: #1d1d1f; - &:first-child { - border-top: none; - } - - &.Mui-expanded { - border-bottom: none !important; - &:last-child { - border-bottom: none; - } - } - - .MuiAccordionDetails-root { - padding: 16px; - } - } -` - -export const StyledTransactionsGroup = styled.div` - align-items: flex-start; - display: flex; - flex-direction: column; - justify-content: flex-start; - margin: 16px 8px; - width: 98%; -` - -export const H2 = styled.h2` - text-transform: uppercase; - font-size: smaller; -` - export const SubTitle = styled.p` margin: 16px 0px 8px 0px; font-size: 0.76em; @@ -129,76 +86,6 @@ export const SubTitle = styled.p` text-transform: uppercase; ` -export const StyledTransactions = styled.div` - background-color: #121212; - border-radius: 8px; - box-shadow: #00000026 0 4px 12px 0; - overflow: hidden; - width: 100%; - - & > .MuiAccordion-root { - &:first-child { - border-top: none; - } - - &:last-child { - border-bottom: none; - } - - &:last-of-type { - div { - row-gap: 0px; - } - } - } -` - -export const GroupedTransactionsCard = styled(StyledTransactions)` - transition: all 150ms cubic-bezier(0.4, 0, 0.2, 1) 0ms; - background-color: transparent; - border-radius: 0; - box-shadow: none; - - &:not(:last-child) { - border-bottom: none; - } - - .MuiAccordion-root, - .MuiAccordionSummary-root, - .MuiAccordionDetails-root { - background-color: transparent; - - &:hover, - &.Mui-expanded { - background-color: transparent; - border-bottom: none !important; - } - } - - // &:hover { - // background-color: ${({ theme }) => theme.colors.background}; - - // .MuiAccordionDetails-root { - // div[class^='tx-'] { - // background-color: ${({ theme }) => theme.colors.background}; - // } - // } - - // .disclaimer-container { - // background-color: ${({ theme }) => theme.colors.inputField}; - // } - // } -` -const gridColumns = { - // nonce: '0.5fr', - type: '3fr', - info: '3fr', - time: '2.5fr', - votes: '1.5fr', - actions: '1.5fr', - status: '2.5fr', -} - const willBeReplaced = css` .will-be-replaced * { color: gray !important; @@ -319,114 +206,6 @@ export const StyledTransaction = styled.div<{ shouldBlur?: boolean }>` } ` -export const StyledGroupedTransactions = styled(StyledTransaction)` - // no \`tx-nonce\` column required - grid-template-columns: ${Object.values(gridColumns).slice(1).join(' ')}; - font-family: 'Inter !important'; -` - -export const GroupedTransactions = styled(StyledTransaction)` - // add a bottom division line for all elements but the last - &:not(:last-of-type) { - border-bottom: none; - } - - // builds the tree-view layout - .tree-lines { - height: 100%; - margin-left: 30px; - position: relative; - width: 30%; - - // this is a special case, the first element in the list needs to have a block child component - // add tree lines line to the first item of the list - .first-node { - display: block; - position: absolute; - top: -16px; - width: 100%; - - &::before { - border-bottom: none; - border-left: none; - content: ''; - height: 22px; - position: absolute; - top: 8px; - width: 100%; - } - } - - // add tree lines to all elements of the list (except for the last one) - // :last-of-type won't work with classes selector (HTML elements only) - // as we need block-level elements, we're using paragraphs for .tree-lines and .first-node - // given that divs are already being used for the transaction row, and both (p and div) are siblings - &:not(:last-of-type) { - &::before { - border-bottom: none; - border-left: none; - content: ''; - height: 100%; - margin-top: 14px; - position: absolute; - width: 100%; - } - } - } - - // overrides Accordion styles, as grouped txs behave differently - > .MuiAccordion-root { - transition: none; - border: 0; - grid-column-end: span 6; - grid-column-start: 2; - - &:first-child { - border: 0; - } - - &.Mui-expanded { - justify-self: center; - width: calc(100% - 32px); - background-color: rgba(62, 63, 64, 1) !important; - &:not(:last-of-type) { - border-bottom: none; - } - - &:not(:first-of-type) { - border-top: none; - // if two consecutive accordions are expanded, borders will get duplicated - // this rule is to overlap them - margin-top: -2px; - } - - > .MuiAccordionSummary-root { - background-color: #0e0e0f !important; - border-bottom: none !important; - border-top: none; - padding: 0; - } - } - } -` - -export const DisclaimerContainer = styled(StyledTransaction)` - background-color: ${({ theme }) => theme.colors.inputField} !important; - border-radius: 4px; - margin: 12px 8px 0 12px; - padding: 8px 12px; - width: calc(100% - 48px); - - .nonce { - grid-column-start: 1; - } - - .disclaimer { - grid-column-start: 2; - grid-column-end: span 6; - } -` - export const TxDetailsContainer = styled.div<{ ownerRows?: number }>` ${willBeReplaced}; @@ -652,11 +431,6 @@ export const InlineEthHashInfo = styled(EthHashInfo)` } ` -export const StyledScrollableBar = styled.div` - scrollbar-color: darkgrey #dadada; - scrollbar-width: thin; -` - export const ScrollableTransactionsContainer = styled.div` overflow-x: hidden; overflow-y: auto; @@ -701,14 +475,6 @@ export const StyledAccordionSummary = styled(AccordionSummary)` min-width: 80px; } ` -export const AlignItemsWithMargin = styled.div` - display: flex; - align-items: center; - - span:first-child { - margin-right: 6px; - } -` export const NoTransactions = styled.div` display: flex; flex-direction: column; diff --git a/src/pages/Voting/ReviewTxPopup.tsx b/src/pages/Voting/ReviewTxPopup.tsx index da2092b477..60d4063a35 100644 --- a/src/pages/Voting/ReviewTxPopup.tsx +++ b/src/pages/Voting/ReviewTxPopup.tsx @@ -7,13 +7,7 @@ import Gap from 'src/components/Gap' import { Popup } from 'src/components/Popup' import Footer from 'src/components/Popup/Footer' import Header from 'src/components/Popup/Header' -import { - getChainDefaultGas, - getChainDefaultGasPrice, - getChainInfo, - getCoinDecimal, - getNativeCurrency, -} from 'src/config' +import { getChainDefaultGas, getChainDefaultGasPrice, getCoinDecimal, getNativeCurrency } from 'src/config' import { MsgTypeUrl } from 'src/logic/providers/constants/constant' import calculateGasFee from 'src/logic/providers/utils/fee' import { extractSafeAddress } from 'src/routes/routes' @@ -29,10 +23,6 @@ const voteMapping = { ['NO']: 3, ['NOWITHVETO']: 4, } -export type VotingTx = { - option: number - proposalId: number -} type ReviewVotingTxProps = { open: boolean diff --git a/src/routes/CancelSafePage/fields/_loadFields.tsx b/src/routes/CancelSafePage/fields/_loadFields.tsx deleted file mode 100644 index b7b0bc16d4..0000000000 --- a/src/routes/CancelSafePage/fields/_loadFields.tsx +++ /dev/null @@ -1,22 +0,0 @@ -export const FIELD_ALLOW_CUSTOM_SAFE_NAME = 'customSafeName' -export const FIELD_ALLOW_SAFE_ADDRESS = 'customSafeAdress' -export const FIELD_ALLOW_SUGGESTED_SAFE_NAME = 'suggestedSafeName' -export const FIELD_ALLOW_IS_LOADING_SAFE_ADDRESS = 'isLoadingSafeAddress' -export const FIELD_SAFE_OWNER_LIST = 'safeOwnerList' -export const FIELD_SAFE_THRESHOLD = 'safeThreshold' -export const FIELD_SAFE_CREATED_ADDRESS = 'safeCreatedAddress' - -export type OwnerFieldListItem = { - address: string - name: string -} - -export type LoadSafeFormValues = { - [FIELD_ALLOW_CUSTOM_SAFE_NAME]?: string - [FIELD_ALLOW_SUGGESTED_SAFE_NAME]: string - [FIELD_ALLOW_SAFE_ADDRESS]: string - [FIELD_ALLOW_IS_LOADING_SAFE_ADDRESS]: boolean - [FIELD_SAFE_OWNER_LIST]: Array - [FIELD_SAFE_THRESHOLD]?: number - [FIELD_SAFE_CREATED_ADDRESS]?: string -} diff --git a/src/routes/CancelSafePage/fields/cancelSafeFields.tsx b/src/routes/CancelSafePage/fields/cancelSafeFields.tsx index e69a180314..dfa76a9dcb 100644 --- a/src/routes/CancelSafePage/fields/cancelSafeFields.tsx +++ b/src/routes/CancelSafePage/fields/cancelSafeFields.tsx @@ -20,5 +20,3 @@ export type CancelSafeFormValues = { [FIELD_ALLOW_SAFE_ADDRESS]: string } - -export const SAFE_PENDING_CREATION_STORAGE_KEY = 'NEW_SAFE_PENDING_CREATION_STORAGE_KEY' diff --git a/src/routes/CancelSafePage/fields/utils.ts b/src/routes/CancelSafePage/fields/utils.ts deleted file mode 100644 index 7425c3be11..0000000000 --- a/src/routes/CancelSafePage/fields/utils.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { AddressBookMap } from 'src/logic/addressBook/store/selectors' -import { CancelSafeFormValues } from 'src/routes/CancelSafePage/fields/cancelSafeFields' -import { checksumAddress } from 'src/utils/checksumAddress' -// import { -// FIELD_ALLOW_CUSTOM_SAFE_NAME, -// FIELD_ALLOW_SAFE_ADDRESS, -// FIELD_ALLOW_SUGGESTED_SAFE_NAME, -// LoadSafeFormValues, -// } from './loadFields' - -import { - FIELD_ALLOW_SAFE_ADDRESS, - FIELD_CREATE_CUSTOM_SAFE_NAME, - FIELD_CREATE_SUGGESTED_SAFE_NAME, -} from './cancelSafeFields' - -export function getLoadSafeName(formValues: CancelSafeFormValues, addressBook: AddressBookMap): string { - let safeAddress = formValues[FIELD_ALLOW_SAFE_ADDRESS] || '' - safeAddress = safeAddress && checksumAddress(safeAddress) - - return ( - formValues[FIELD_CREATE_CUSTOM_SAFE_NAME] || - addressBook[safeAddress]?.name || - formValues[FIELD_CREATE_SUGGESTED_SAFE_NAME] - ) -} diff --git a/src/routes/CancelSafePage/steps/ReviewAllowStep.tsx b/src/routes/CancelSafePage/steps/ReviewAllowStep.tsx index e0ea0b88f3..e969362fdd 100644 --- a/src/routes/CancelSafePage/steps/ReviewAllowStep.tsx +++ b/src/routes/CancelSafePage/steps/ReviewAllowStep.tsx @@ -20,7 +20,6 @@ import { FIELD_SAFE_THRESHOLD, } from '../fields/cancelSafeFields' -export const reviewLoadStepLabel = 'Review' function ReviewAllowStep(): ReactElement { const loadSafeForm = useForm() diff --git a/src/routes/CreateSafePage/components/SafeCreationProcess.tsx b/src/routes/CreateSafePage/components/SafeCreationProcess.tsx deleted file mode 100644 index 7be5ef1111..0000000000 --- a/src/routes/CreateSafePage/components/SafeCreationProcess.tsx +++ /dev/null @@ -1,312 +0,0 @@ -import { ReactElement, useState, useEffect, useCallback } from 'react' -import { useDispatch, useSelector } from 'react-redux' -import { backOff } from 'exponential-backoff' -import { TransactionReceipt } from 'web3-core' -import { GenericModal } from '@aura/safe-react-components' -import styled from 'styled-components' - -import { getSafeDeploymentTransaction } from 'src/logic/contracts/safeContracts' -import { txMonitor } from 'src/logic/safe/transactions/txMonitor' -import { userAccountSelector } from 'src/logic/wallets/store/selectors' -import { SafeDeployment } from 'src/routes/opening' -import { useAnalytics, USER_EVENTS } from 'src/utils/googleAnalytics' -import { loadFromStorage, removeFromStorage, saveToStorage } from 'src/utils/storage' -import { addOrUpdateSafe } from 'src/logic/safe/store/actions/addOrUpdateSafe' -import { - SAFE_PENDING_CREATION_STORAGE_KEY, - CreateSafeFormValues, - FIELD_NEW_SAFE_THRESHOLD, - FIELD_SAFE_OWNERS_LIST, - FIELD_NEW_SAFE_GAS_LIMIT, - FIELD_NEW_SAFE_CREATION_TX_HASH, - FIELD_CREATE_SUGGESTED_SAFE_NAME, - FIELD_CREATE_CUSTOM_SAFE_NAME, - FIELD_NEW_SAFE_PROXY_SALT, - FIELD_NEW_SAFE_GAS_PRICE, -} from '../fields/createSafeFields' -import { getSafeInfo } from 'src/logic/safe/utils/safeInformation' -import { buildSafe } from 'src/logic/safe/store/actions/fetchSafe' -import { addressBookSafeLoad } from 'src/logic/addressBook/store/actions' -import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook' -import Paragraph from 'src/components/layout/Paragraph' -import NetworkLabel from 'src/components/NetworkLabel/NetworkLabel' -import Button from 'src/components/layout/Button' -import { boldFont } from 'src/theme/variables' -import { WELCOME_ROUTE, history, generateSafeRoute, SAFE_ROUTES } from 'src/routes/routes' -import { getExplorerInfo, getShortName } from 'src/config' -import { getGasParam } from 'src/logic/safe/transactions/gas' -import { currentChainId } from 'src/logic/config/store/selectors' -import PrefixedEthHashInfo from 'src/components/PrefixedEthHashInfo' - -export const InlinePrefixedEthHashInfo = styled(PrefixedEthHashInfo)` - display: inline-flex; -` - -type ModalDataType = { - safeAddress: string - safeName?: string - safeCreationTxHash?: string -} - -const goToWelcomePage = () => { - history.push(WELCOME_ROUTE) -} - -const sleep = (ms: number): Promise => new Promise((resolve) => setTimeout(resolve, ms)) - -function SafeCreationProcess(): ReactElement { - const [safeCreationTxHash, setSafeCreationTxHash] = useState() - const [creationTxPromise, setCreationTxPromise] = useState>() - - const { trackEvent } = useAnalytics() - const dispatch = useDispatch() - const userAddressAccount = useSelector(userAccountSelector) - const chainId = useSelector(currentChainId) - - const [showModal, setShowModal] = useState(false) - const [modalData, setModalData] = useState({ safeAddress: '' }) - const [showCouldNotLoadModal, setShowCouldNotLoadModal] = useState(false) - const [newSafeAddress, setNewSafeAddress] = useState('') - - const createNewSafe = useCallback(() => { - const safeCreationFormValues = loadFromStorage(SAFE_PENDING_CREATION_STORAGE_KEY) - - if (!safeCreationFormValues) { - goToWelcomePage() - return - } - - setSafeCreationTxHash(safeCreationFormValues[FIELD_NEW_SAFE_CREATION_TX_HASH]) - - setCreationTxPromise( - new Promise((resolve, reject) => { - const confirmations = safeCreationFormValues[FIELD_NEW_SAFE_THRESHOLD] - const ownerFields = safeCreationFormValues[FIELD_SAFE_OWNERS_LIST] - const ownerAddresses = ownerFields.map(({ addressFieldName }) => safeCreationFormValues[addressFieldName]) - const safeCreationSalt = safeCreationFormValues[FIELD_NEW_SAFE_PROXY_SALT] - const gasLimit = safeCreationFormValues[FIELD_NEW_SAFE_GAS_LIMIT] - const gasPrice = safeCreationFormValues[FIELD_NEW_SAFE_GAS_PRICE] - const deploymentTx = getSafeDeploymentTransaction(ownerAddresses, confirmations, safeCreationSalt) - - deploymentTx - .send({ - from: userAddressAccount, - gas: gasLimit, - [getGasParam()]: gasPrice, - }) - .once('transactionHash', (txHash) => { - saveToStorage(SAFE_PENDING_CREATION_STORAGE_KEY, { - [FIELD_NEW_SAFE_CREATION_TX_HASH]: txHash, - ...safeCreationFormValues, - }) - - // Monitor the latest block to find a potential speed-up tx - txMonitor({ sender: userAddressAccount, hash: txHash, data: deploymentTx.encodeABI() }) - .then((txReceipt) => { - console.log('Speed up tx mined:', txReceipt) - resolve(txReceipt) - }) - .catch((error) => { - reject(error) - }) - }) - .then((txReceipt) => { - console.log('First tx mined:', txReceipt) - resolve(txReceipt) - }) - .catch((error) => { - reject(error) - }) - }), - ) - }, [userAddressAccount]) - - useEffect(() => { - const safeCreationFormValues = loadFromStorage(SAFE_PENDING_CREATION_STORAGE_KEY) - if (!safeCreationFormValues) { - goToWelcomePage() - return - } - - const safeCreationTxHash = safeCreationFormValues[FIELD_NEW_SAFE_CREATION_TX_HASH] - if (safeCreationTxHash) { - setSafeCreationTxHash(safeCreationTxHash) - } else { - createNewSafe() - } - }, [createNewSafe]) - - const onSafeCreated = async (newSafeAddress: string): Promise => { - const createSafeFormValues = loadFromStorage(SAFE_PENDING_CREATION_STORAGE_KEY) - - if (!createSafeFormValues) { - goToWelcomePage() - return - } - - const safeCreationTxHash = createSafeFormValues[FIELD_NEW_SAFE_CREATION_TX_HASH] - const defaultSafeValue = createSafeFormValues[FIELD_CREATE_SUGGESTED_SAFE_NAME] - const safeName = createSafeFormValues[FIELD_CREATE_CUSTOM_SAFE_NAME] || defaultSafeValue - const owners = createSafeFormValues[FIELD_SAFE_OWNERS_LIST] - - // we update the address book with the owners and the new safe - const ownersAddressBookEntry = owners.map(({ nameFieldName, addressFieldName }) => - makeAddressBookEntry({ - address: createSafeFormValues[addressFieldName], - name: createSafeFormValues[nameFieldName], - chainId, - }), - ) - const safeAddressBookEntry = makeAddressBookEntry({ address: newSafeAddress, name: safeName, chainId }) - await dispatch(addressBookSafeLoad([...ownersAddressBookEntry, safeAddressBookEntry])) - - trackEvent(USER_EVENTS.CREATE_SAFE) - - // a default 5s wait before starting to request safe information - await sleep(5000) - - try { - // exponential delay between attempts for around 4 min - await backOff(() => getSafeInfo(newSafeAddress), { - startingDelay: 750, - maxDelay: 20000, - numOfAttempts: 19, - retry: (e) => { - console.info('waiting for client-gateway to provide safe information', e) - return true - }, - }) - } catch (e) { - setNewSafeAddress(newSafeAddress) - setShowCouldNotLoadModal(true) - return - } - - const safeProps = await buildSafe(newSafeAddress) - await dispatch(addOrUpdateSafe(safeProps)) - - setShowModal(true) - setModalData({ - safeAddress: safeProps.address, - safeName, - safeCreationTxHash, - }) - } - - const onRetry = (): void => { - const safeCreationFormValues = loadFromStorage(SAFE_PENDING_CREATION_STORAGE_KEY) - - if (!safeCreationFormValues) { - goToWelcomePage() - return - } - - setSafeCreationTxHash(undefined) - delete safeCreationFormValues.safeCreationTxHash - saveToStorage(SAFE_PENDING_CREATION_STORAGE_KEY, safeCreationFormValues) - createNewSafe() - } - - const onCancel = () => { - removeFromStorage(SAFE_PENDING_CREATION_STORAGE_KEY) - goToWelcomePage() - } - - function onClickModalButton() { - removeFromStorage(SAFE_PENDING_CREATION_STORAGE_KEY) - - const { safeName, safeCreationTxHash, safeAddress } = modalData - history.push({ - pathname: generateSafeRoute(SAFE_ROUTES.ASSETS_BALANCES, { - shortName: getShortName(), - safeAddress, - }), - state: { - name: safeName, - tx: safeCreationTxHash, - }, - }) - } - - return ( - <> - - {showModal && ( - - - You just created a new Safe on - - - You will only be able to use this Safe on - - - If you send assets on other networks to this address,{' '} - you will not be able to access them - - - } - footer={ - - - - } - /> - )} - {showCouldNotLoadModal && newSafeAddress && ( - - - We are currently unable to load the Safe but it was successfully created and can be found
- under the following address{' '} - -
- - } - footer={ - - - - } - /> - )} - - ) -} - -export default SafeCreationProcess - -const ButtonContainer = styled.div` - text-align: center; -` - -const EmphasisLabel = styled.span` - font-weight: ${boldFont}; -` diff --git a/src/routes/CreateSafePage/fields/createSafeFields.tsx b/src/routes/CreateSafePage/fields/createSafeFields.tsx index b4dcc9fc8c..7b27ccaba1 100644 --- a/src/routes/CreateSafePage/fields/createSafeFields.tsx +++ b/src/routes/CreateSafePage/fields/createSafeFields.tsx @@ -8,7 +8,7 @@ export const FIELD_NEW_SAFE_GAS_LIMIT = 'gasLimit' export const FIELD_NEW_SAFE_GAS_PRICE = 'gasPrice' export const FIELD_NEW_SAFE_CREATION_TX_HASH = 'safeCreationTxHash' -export type OwnerFieldItem = { +type OwnerFieldItem = { nameFieldName: string addressFieldName: string } diff --git a/src/routes/CreateSafePage/steps/NameNewSafeStep/styles.tsx b/src/routes/CreateSafePage/steps/NameNewSafeStep/styles.tsx index 368d234e0e..8c9c4b6abc 100644 --- a/src/routes/CreateSafePage/steps/NameNewSafeStep/styles.tsx +++ b/src/routes/CreateSafePage/steps/NameNewSafeStep/styles.tsx @@ -12,7 +12,4 @@ const FieldContainer = styled(Block)` margin-top: 12px; ` -const Link = styled.a` - color: ${green}; -` -export { BlockWithPadding, FieldContainer, Link } +export { BlockWithPadding, FieldContainer } diff --git a/src/routes/CreateSafePage/steps/OwnersAndConfirmationsNewSafeStep/styles.tsx b/src/routes/CreateSafePage/steps/OwnersAndConfirmationsNewSafeStep/styles.tsx index 973cbf0bda..b1a8deca63 100644 --- a/src/routes/CreateSafePage/steps/OwnersAndConfirmationsNewSafeStep/styles.tsx +++ b/src/routes/CreateSafePage/steps/OwnersAndConfirmationsNewSafeStep/styles.tsx @@ -1,12 +1,11 @@ -import { disabled, extraSmallFontSize, lg, sm, xs, mediumFont, descriptionAura } from 'src/theme/variables' -import Block from 'src/components/layout/Block' import CheckCircle from '@material-ui/icons/CheckCircle' -import Paragraph from 'src/components/layout/Paragraph' -import styled from 'styled-components' -import { Link } from '@aura/safe-react-components' -import Row from 'src/components/layout/Row' import Field from 'src/components/forms/Field' +import Block from 'src/components/layout/Block' import Col from 'src/components/layout/Col' +import Paragraph from 'src/components/layout/Paragraph' +import Row from 'src/components/layout/Row' +import { descriptionAura, extraSmallFontSize, lg, mediumFont, sm } from 'src/theme/variables' +import styled from 'styled-components' const BlockWithPadding = styled(Block)` padding: ${lg}; @@ -16,16 +15,6 @@ const ParagraphWithMargin = styled(Paragraph)` margin-bottom: 12px; ` -const StyledLink = styled(Link)` - padding: 0 ${xs}; - & svg { - position: relative; - top: 1px; - left: ${xs}; - height: 14px; - width: 14px; - } -` const RowHeader = styled(Row)` padding: ${sm} ${lg}; font-size: ${extraSmallFontSize}; @@ -70,7 +59,6 @@ const FieldStyled = styled(Field)` export { BlockWithPadding, ParagraphWithMargin, - StyledLink, RowHeader, OwnerNameField, CheckIconAddressAdornment, diff --git a/src/routes/CreateSafePage/styles.tsx b/src/routes/CreateSafePage/styles.tsx index c3c3256531..898f145b87 100644 --- a/src/routes/CreateSafePage/styles.tsx +++ b/src/routes/CreateSafePage/styles.tsx @@ -20,14 +20,6 @@ const EmphasisLabel = styled.span` font-weight: ${boldFont}; ` -const ButtonContainer = styled.div` - text-align: center; -` - -const StyledGenericModal = styled(GenericModal)` - background-color: ${bgBox}; -` - const StyledBorder = styled.div` border-radius: 50px !important; border: 2px solid transparent; @@ -46,13 +38,4 @@ const StyledButtonLabel = styled(Text)` background-color: transparent !important; ` -export { - LoaderContainer, - BackIcon, - EmphasisLabel, - ButtonContainer, - StyledGenericModal, - StyledButtonBorder, - StyledBorder, - StyledButtonLabel, -} +export { LoaderContainer, BackIcon, EmphasisLabel, StyledButtonBorder, StyledBorder, StyledButtonLabel } diff --git a/src/routes/LoadSafePage/fields/loadFields.tsx b/src/routes/LoadSafePage/fields/loadFields.tsx index 41a434b0d1..f985907b07 100644 --- a/src/routes/LoadSafePage/fields/loadFields.tsx +++ b/src/routes/LoadSafePage/fields/loadFields.tsx @@ -6,7 +6,7 @@ export const FIELD_LOAD_IS_LOADING_SAFE_ADDRESS = 'isLoadingSafeAddress' export const FIELD_SAFE_OWNER_LIST = 'safeOwnerList' export const FIELD_SAFE_THRESHOLD = 'safeThreshold' -export type OwnerFieldListItem = { +type OwnerFieldListItem = { address: string name: string } diff --git a/src/routes/LoadSafePage/steps/LoadSafeAddressStep/styles.tsx b/src/routes/LoadSafePage/steps/LoadSafeAddressStep/styles.tsx index 52ee1afc14..9901835bfb 100644 --- a/src/routes/LoadSafePage/steps/LoadSafeAddressStep/styles.tsx +++ b/src/routes/LoadSafePage/steps/LoadSafeAddressStep/styles.tsx @@ -17,9 +17,4 @@ const CheckIconAddressAdornment = styled(CheckCircle)` color: #03ae60; height: 20px; ` - -const StyledLink = styled.a` - color: ${green}; -` - -export { Container, FieldContainer, CheckIconAddressAdornment, StyledLink } +export { Container, FieldContainer, CheckIconAddressAdornment } diff --git a/src/routes/routes.ts b/src/routes/routes.ts index e52df568a9..97d63dd751 100644 --- a/src/routes/routes.ts +++ b/src/routes/routes.ts @@ -22,19 +22,18 @@ export const SAFE_ADDRESS_SLUG = 'prefixedSafeAddress' export const ADDRESSED_ROUTE = `/:${SAFE_ADDRESS_SLUG}(${chainSpecificSafeAddressPathRegExp})` // Safe section routes, i.e. /:prefixedSafeAddress/settings const SAFE_SECTION_SLUG = 'safeSection' -export const SAFE_SECTION_ROUTE = `${ADDRESSED_ROUTE}/:${SAFE_SECTION_SLUG}` +const SAFE_SECTION_ROUTE = `${ADDRESSED_ROUTE}/:${SAFE_SECTION_SLUG}` // Safe section routes, i.e. /:prefixedSafeAddress/settings const VOTING_SECTION_SLUG = 'proposalId' -export const VOTING_SECTION_ROUTE = `${ADDRESSED_ROUTE}/voting/detail/:${VOTING_SECTION_SLUG}` // Safe subsection routes, i.e. /:prefixedSafeAddress/settings/advanced -export const SAFE_SUBSECTION_SLUG = 'safeSubsection' +const SAFE_SUBSECTION_SLUG = 'safeSubsection' export const SAFE_SUBSECTION_ROUTE = `${SAFE_SECTION_ROUTE}/:${SAFE_SUBSECTION_SLUG}` export const TRANSACTION_ID_SLUG = `safeTxHash` -export const TRANSACTION_ID_NUMBER = `id` -export const VOTING_ID_NUMBER = `proposalId` +const TRANSACTION_ID_NUMBER = `id` +const VOTING_ID_NUMBER = `proposalId` // URL: gnosis-safe.io/app/:[SAFE_ADDRESS_SLUG]/:[SAFE_SECTION_SLUG]/:[SAFE_SUBSECTION_SLUG] export type SafeRouteSlugs = { @@ -90,11 +89,6 @@ export const getNetworkRootRoutes = (): Array<{ chainId: ChainId; route: string })) export type SafeRouteParams = { shortName: ShortName; safeAddress: string; safeId?: number; proposalId?: number } - -export const isValidShortChainName = (shortName: ShortName): boolean => { - return getChains().some((chain) => chain.shortName === shortName) -} - // Due to hoisting issues, these functions should remain here export const extractPrefixedSafeAddress = ( path = history.location.pathname, diff --git a/src/routes/safe/components/Apps/components/AppCard/index.stories.tsx b/src/routes/safe/components/Apps/components/AppCard/index.stories.tsx deleted file mode 100644 index 752ffbe295..0000000000 --- a/src/routes/safe/components/Apps/components/AppCard/index.stories.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { FETCH_STATUS } from 'src/utils/requests' -import { getEmptySafeApp } from '../../utils' -import { AppCard, AddCustomAppCard } from './index' - -export default { - title: 'Apps/AppCard', - component: AppCard, -} - -export const Loading = (): React.ReactElement => - -export const AddCustomApp = (): React.ReactElement => {}} /> - -export const LoadedApp = (): React.ReactElement => ( - -) diff --git a/src/routes/safe/components/Balances/SendModal/index.tsx b/src/routes/safe/components/Balances/SendModal/index.tsx index 18dd1d0d9d..594c823654 100644 --- a/src/routes/safe/components/Balances/SendModal/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/index.tsx @@ -41,7 +41,7 @@ const useStyles = makeStyles({ }, }) -export type TxType = +type TxType = | 'chooseTxType' | 'sendFunds' | 'sendFundsReviewTx' diff --git a/src/routes/safe/components/Balances/SendModal/screens/AddressBookInput/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/AddressBookInput/index.tsx index 36eab34db1..bbd8aecdd0 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/AddressBookInput/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/AddressBookInput/index.tsx @@ -22,7 +22,7 @@ import { FEATURES } from '@gnosis.pm/safe-react-gateway-sdk' import { parsePrefixedAddress } from 'src/utils/prefixedAddress' import { hasFeature } from 'src/logic/safe/utils/safeVersion' -export interface AddressBookProps { +interface AddressBookProps { fieldMutator: (address: string) => void label?: string pristine?: boolean @@ -32,7 +32,7 @@ export interface AddressBookProps { setSelectedEntry: Dispatch | null> } -export interface BaseAddressBookInputProps extends AddressBookProps { +interface BaseAddressBookInputProps extends AddressBookProps { addressBookEntries: AddressBookEntry[] setSelectedEntry: (args: { address: string; name: string } | null) => void setValidationText: Dispatch> diff --git a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/Buttons/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/Buttons/index.tsx index 6b08247251..1cdb398987 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/Buttons/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/Buttons/index.tsx @@ -3,7 +3,7 @@ import { Modal } from 'src/components/Modal' import { ButtonStatus } from 'src/components/Modal/type' import { isReadMethod } from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/utils' -export interface ButtonProps { +interface ButtonProps { onClose: () => void requiresMethod?: boolean } diff --git a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/ContractABI/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/ContractABI/index.tsx index 65f45b1078..df5e46d181 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/ContractABI/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/ContractABI/index.tsx @@ -9,7 +9,7 @@ import { getContractABI } from 'src/config' import { extractUsefulMethods } from 'src/logic/contractInteraction/sources/ABIService' import { parsePrefixedAddress } from 'src/utils/prefixedAddress' -export const NO_DATA = 'no data' +const NO_DATA = 'no data' const hasUsefulMethods = (abi: string): undefined | string => { try { diff --git a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/EthAddressInput/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/EthAddressInput/index.tsx index 3b7dcc26df..91e4890792 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/EthAddressInput/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/EthAddressInput/index.tsx @@ -19,7 +19,7 @@ import { styles } from 'src/routes/safe/components/Balances/SendModal/screens/Co const useStyles = makeStyles(styles) -export interface EthAddressInputProps { +interface EthAddressInputProps { isContract?: boolean isRequired?: boolean name: string diff --git a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/utils/index.ts b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/utils/index.ts index 5167a947ed..7026a5b90d 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/utils/index.ts +++ b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/utils/index.ts @@ -60,13 +60,12 @@ export const formMutators: Record type.indexOf('address') === 0 export const isBoolean = (type: string): boolean => type.indexOf('bool') === 0 -export const isString = (type: string): boolean => type.indexOf('string') === 0 export const isUint = (type: string): boolean => type.indexOf('uint') === 0 export const isInt = (type: string): boolean => type.indexOf('int') === 0 export const isByte = (type: string): boolean => type.indexOf('byte') === 0 export const isArrayParameter = (parameter: string): boolean => /(\[\d*])+$/.test(parameter) -export const getParsedJSONOrArrayFromString = (parameter: string): (string | number)[] | null => { +const getParsedJSONOrArrayFromString = (parameter: string): (string | number)[] | null => { try { const arrayResult = JSON.parse(parameter) return arrayResult.map((value) => { diff --git a/src/routes/safe/components/Balances/dataFetcher.ts b/src/routes/safe/components/Balances/dataFetcher.ts index 8665a22ba7..365cde4e0f 100644 --- a/src/routes/safe/components/Balances/dataFetcher.ts +++ b/src/routes/safe/components/Balances/dataFetcher.ts @@ -5,7 +5,7 @@ import { formatAmountInUsFormat } from 'src/logic/tokens/utils/formatAmount' import { formatNativeCurrency } from 'src/utils' export const BALANCE_TABLE_ASSET_ID = 'asset' export const BALANCE_TABLE_BALANCE_ID = 'balance' -export const BALANCE_TABLE_VALUE_ID = 'value' +const BALANCE_TABLE_VALUE_ID = 'value' const getTokenPriceInCurrency = (balance: string, currencySelected?: string): string => { if (!currencySelected) { diff --git a/src/routes/safe/components/Balances/utils/index.ts b/src/routes/safe/components/Balances/utils/index.ts index f91f4984b3..1b9df3f56f 100644 --- a/src/routes/safe/components/Balances/utils/index.ts +++ b/src/routes/safe/components/Balances/utils/index.ts @@ -1,2 +1 @@ export { setImageToPlaceholder } from './setTokenImgToPlaceholder' -export { setCollectibleImageToPlaceholder } from './setCollectibleImageToPlaceholder' diff --git a/src/routes/safe/components/SafeLoadError.tsx b/src/routes/safe/components/SafeLoadError.tsx index 2be88a4383..c3e0b58374 100644 --- a/src/routes/safe/components/SafeLoadError.tsx +++ b/src/routes/safe/components/SafeLoadError.tsx @@ -34,7 +34,7 @@ const SafeLoadError = (): ReactElement => { ) } -export const ErrorContainer = styled.div` +const ErrorContainer = styled.div` width: 100%; height: 100%; display: flex; diff --git a/src/routes/safe/components/Settings/Advanced/TransactionGuard.tsx b/src/routes/safe/components/Settings/Advanced/TransactionGuard.tsx index 03f99b83ff..755bbb6d37 100644 --- a/src/routes/safe/components/Settings/Advanced/TransactionGuard.tsx +++ b/src/routes/safe/components/Settings/Advanced/TransactionGuard.tsx @@ -18,7 +18,7 @@ import PrefixedEthHashInfo from 'src/components/PrefixedEthHashInfo' import { getExplorerInfo } from 'src/config' export const REMOVE_GUARD_BTN_TEST_ID = 'remove-guard-btn' -export const GUARDS_ROW_TEST_ID = 'guards-row' +const GUARDS_ROW_TEST_ID = 'guards-row' interface TransactionGuardProps { address: string diff --git a/src/routes/safe/components/Settings/Advanced/dataFetcher.ts b/src/routes/safe/components/Settings/Advanced/dataFetcher.ts index 88ae7b3572..066b59670e 100644 --- a/src/routes/safe/components/Settings/Advanced/dataFetcher.ts +++ b/src/routes/safe/components/Settings/Advanced/dataFetcher.ts @@ -3,7 +3,7 @@ import { TableColumn } from 'src/components/Table/types.d' import { ModulePair } from 'src/logic/safe/store/models/safe' export const MODULES_TABLE_ADDRESS_ID = 'address' -export const MODULES_TABLE_ACTIONS_ID = 'actions' +const MODULES_TABLE_ACTIONS_ID = 'actions' export type ModuleAddressColumn = { [MODULES_TABLE_ADDRESS_ID]: ModulePair }[] diff --git a/src/routes/safe/components/Settings/Appearance/index.tsx b/src/routes/safe/components/Settings/Appearance/index.tsx deleted file mode 100644 index 7c7bac9c44..0000000000 --- a/src/routes/safe/components/Settings/Appearance/index.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import FormGroup from '@material-ui/core/FormGroup/FormGroup' -import FormControlLabel from '@material-ui/core/FormControlLabel/FormControlLabel' -import { ChangeEvent, ReactElement, useEffect } from 'react' -import Heading from 'src/components/layout/Heading' -import Paragraph from 'src/components/layout/Paragraph' -import { copyShortNameSelector, showShortNameSelector } from 'src/logic/appearance/selectors' -import { useDispatch, useSelector } from 'react-redux' -import { setShowShortName } from 'src/logic/appearance/actions/setShowShortName' -import { setCopyShortName } from 'src/logic/appearance/actions/setCopyShortName' -import { extractSafeAddress } from 'src/routes/routes' -import { useAnalytics, SETTINGS_EVENTS } from 'src/utils/googleAnalytics' -import { Container, StyledPrefixedEthHashInfo } from './styles' -import { makeStyles } from '@material-ui/core' -import { styles } from './../style' -import { Checkbox } from '@aura/safe-react-components' -// Other settings sections use MUI createStyles .container -// will adjust that during dark mode implementation - -const useStyles = makeStyles(styles) - -const Appearance = (): ReactElement => { - const dispatch = useDispatch() - const copyShortName = useSelector(copyShortNameSelector) - const showShortName = useSelector(showShortNameSelector) - const safeAddress = extractSafeAddress() - - const classes = useStyles() - - const { trackEvent } = useAnalytics() - - useEffect(() => { - trackEvent(SETTINGS_EVENTS.APPEARANCE) - }, [trackEvent]) - - const handleShowChange = (_: ChangeEvent, checked: boolean) => { - dispatch(setShowShortName({ showShortName: checked })) - - const label = `${SETTINGS_EVENTS.APPEARANCE.label} (${checked ? 'Enable' : 'Disable'} EIP-3770 prefixes)` - trackEvent({ ...SETTINGS_EVENTS.APPEARANCE, label }) - } - const handleCopyChange = (_: ChangeEvent, checked: boolean) => - dispatch(setCopyShortName({ copyShortName: checked })) - - return ( - - Use Chain-Specific Addresses - You can choose whether to prepend EIP-3770 short chain names accross Safes. - - - } - label="Prepend addresses with chain prefix." - /> - } - /> - - - ) -} - -export default Appearance diff --git a/src/routes/safe/components/Settings/Appearance/styles.tsx b/src/routes/safe/components/Settings/Appearance/styles.tsx deleted file mode 100644 index b3cf1ba35c..0000000000 --- a/src/routes/safe/components/Settings/Appearance/styles.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import PrefixedEthHashInfo from 'src/components/PrefixedEthHashInfo' -import Block from 'src/components/layout/Block' -import styled from 'styled-components' -import { lg, bgBox } from 'src/theme/variables' - -const Container = styled(Block)` - padding: ${lg}; - background-color: ${bgBox}; - border-radius: 8px; -` - -const StyledPrefixedEthHashInfo = styled(PrefixedEthHashInfo)` - margin-bottom: 1em; -` -export { Container, StyledPrefixedEthHashInfo } diff --git a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/index.tsx b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/index.tsx index 739cda249c..91ae9b033c 100644 --- a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/index.tsx @@ -27,7 +27,7 @@ export type OwnerValues = { threshold: string } -export const sendAddOwner = async ( +const sendAddOwner = async ( values: OwnerValues, safeAddress: string, safeVersion: string, diff --git a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/OwnerForm/index.tsx b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/OwnerForm/index.tsx index 634da9e8ec..83f283f086 100644 --- a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/OwnerForm/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/OwnerForm/index.tsx @@ -31,9 +31,9 @@ import { OwnerValues } from '../..' import { Modal } from 'src/components/Modal' import { ModalHeader } from 'src/routes/safe/components/Balances/SendModal/screens/ModalHeader' -export const ADD_OWNER_NAME_INPUT_TEST_ID = 'add-owner-name-input' -export const ADD_OWNER_ADDRESS_INPUT_TEST_ID = 'add-owner-address-testid' -export const ADD_OWNER_NEXT_BTN_TEST_ID = 'add-owner-next-btn' +const ADD_OWNER_NAME_INPUT_TEST_ID = 'add-owner-name-input' +const ADD_OWNER_ADDRESS_INPUT_TEST_ID = 'add-owner-address-testid' +const ADD_OWNER_NEXT_BTN_TEST_ID = 'add-owner-next-btn' const formMutators: Record< string, diff --git a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/index.tsx b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/index.tsx index 30f062c389..45b3c1a150 100644 --- a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/index.tsx @@ -25,7 +25,7 @@ import { ModalHeader } from 'src/routes/safe/components/Balances/SendModal/scree import { getSafeSDK } from 'src/logic/wallets/getWeb3' import { Errors, logError } from 'src/logic/exceptions/CodedException' -export const ADD_OWNER_SUBMIT_BTN_TEST_ID = 'add-owner-submit-btn' +const ADD_OWNER_SUBMIT_BTN_TEST_ID = 'add-owner-submit-btn' const useStyles = makeStyles(styles) diff --git a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/ThresholdForm/index.tsx b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/ThresholdForm/index.tsx index 616544662d..c3689d0e18 100644 --- a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/ThresholdForm/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/ThresholdForm/index.tsx @@ -18,7 +18,7 @@ import { currentSafe } from 'src/logic/safe/store/selectors' import { Modal } from 'src/components/Modal' import { ModalHeader } from 'src/routes/safe/components/Balances/SendModal/screens/ModalHeader' -export const ADD_OWNER_THRESHOLD_NEXT_BTN_TEST_ID = 'add-owner-threshold-next-btn' +const ADD_OWNER_THRESHOLD_NEXT_BTN_TEST_ID = 'add-owner-threshold-next-btn' const useStyles = makeStyles(styles) diff --git a/src/routes/safe/components/Settings/ManageOwners/EditOwnerModal/index.tsx b/src/routes/safe/components/Settings/ManageOwners/EditOwnerModal/index.tsx index c0aa57830d..3ea94e7cb0 100644 --- a/src/routes/safe/components/Settings/ManageOwners/EditOwnerModal/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/EditOwnerModal/index.tsx @@ -20,8 +20,8 @@ import { OwnerData } from 'src/routes/safe/components/Settings/ManageOwners/data import { useStyles } from './style' import { currentChainId } from 'src/logic/config/store/selectors' -export const RENAME_OWNER_INPUT_TEST_ID = 'rename-owner-input' -export const SAVE_OWNER_CHANGES_BTN_TEST_ID = 'save-owner-changes-btn' +const RENAME_OWNER_INPUT_TEST_ID = 'rename-owner-input' +const SAVE_OWNER_CHANGES_BTN_TEST_ID = 'save-owner-changes-btn' type OwnProps = { isOpen: boolean diff --git a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/index.tsx b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/index.tsx index 1a673e11f1..61fd3088f9 100644 --- a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/index.tsx @@ -21,7 +21,7 @@ type OwnerValues = OwnerData & { threshold: string } -export const sendRemoveOwner = async ( +const sendRemoveOwner = async ( values: OwnerValues, safeAddress: string, safeVersion: string, diff --git a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/CheckOwner/index.tsx b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/CheckOwner/index.tsx index f393dda742..06899aaa6e 100644 --- a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/CheckOwner/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/CheckOwner/index.tsx @@ -13,7 +13,7 @@ import { OwnerData } from 'src/routes/safe/components/Settings/ManageOwners/data import { useStyles } from './style' -export const REMOVE_OWNER_MODAL_NEXT_BTN_TEST_ID = 'remove-owner-next-btn' +const REMOVE_OWNER_MODAL_NEXT_BTN_TEST_ID = 'remove-owner-next-btn' interface CheckOwnerProps { onClose: () => void diff --git a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/Review/index.tsx b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/Review/index.tsx index 60abf858be..0b907eeac9 100644 --- a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/Review/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/Review/index.tsx @@ -26,7 +26,7 @@ import { getSafeSDK } from 'src/logic/wallets/getWeb3' import { logError } from 'src/logic/exceptions/CodedException' import ErrorCodes from 'src/logic/exceptions/registry' -export const REMOVE_OWNER_REVIEW_BTN_TEST_ID = 'remove-owner-review-btn' +const REMOVE_OWNER_REVIEW_BTN_TEST_ID = 'remove-owner-review-btn' type ReviewRemoveOwnerProps = { onClickBack: () => void diff --git a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/ThresholdForm/index.tsx b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/ThresholdForm/index.tsx index dd2843c7ff..ed57305f78 100644 --- a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/ThresholdForm/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/ThresholdForm/index.tsx @@ -18,7 +18,7 @@ import { currentSafe } from 'src/logic/safe/store/selectors' import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters' import { ModalHeader } from 'src/routes/safe/components/Balances/SendModal/screens/ModalHeader' -export const REMOVE_OWNER_THRESHOLD_NEXT_BTN_TEST_ID = 'remove-owner-threshold-next-btn' +const REMOVE_OWNER_THRESHOLD_NEXT_BTN_TEST_ID = 'remove-owner-threshold-next-btn' type ThresholdValues = { threshold: string diff --git a/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/index.tsx b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/index.tsx index 59ed04da02..7ba988cc04 100644 --- a/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/index.tsx @@ -26,7 +26,7 @@ export type OwnerValues = { name: string } -export const sendReplaceOwner = async ( +const sendReplaceOwner = async ( newOwner: OwnerValues, safeAddress: string, safeVersion: string, diff --git a/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/OwnerForm/index.tsx b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/OwnerForm/index.tsx index dd46c69452..5bc9d3ad22 100644 --- a/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/OwnerForm/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/OwnerForm/index.tsx @@ -30,12 +30,12 @@ import { isValidAddress } from 'src/utils/isValidAddress' import { useStyles } from './style' import { getExplorerInfo } from 'src/config' -export const REPLACE_OWNER_NAME_INPUT_TEST_ID = 'replace-owner-name-input' -export const REPLACE_OWNER_ADDRESS_INPUT_TEST_ID = 'replace-owner-address-testid' -export const REPLACE_OWNER_NEXT_BTN_TEST_ID = 'replace-owner-next-btn' import { OwnerValues } from '../..' import { ModalHeader } from 'src/routes/safe/components/Balances/SendModal/screens/ModalHeader' +const REPLACE_OWNER_NAME_INPUT_TEST_ID = 'replace-owner-name-input' +const REPLACE_OWNER_ADDRESS_INPUT_TEST_ID = 'replace-owner-address-testid' +const REPLACE_OWNER_NEXT_BTN_TEST_ID = 'replace-owner-next-btn' const formMutators: Record< string, diff --git a/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/Review/index.tsx b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/Review/index.tsx index d954ba944a..e4bd560ed8 100644 --- a/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/Review/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/Review/index.tsx @@ -25,7 +25,7 @@ import { ModalHeader } from 'src/routes/safe/components/Balances/SendModal/scree import { getSafeSDK } from 'src/logic/wallets/getWeb3' import { Errors, logError } from 'src/logic/exceptions/CodedException' -export const REPLACE_OWNER_SUBMIT_BTN_TEST_ID = 'replace-owner-submit-btn' +const REPLACE_OWNER_SUBMIT_BTN_TEST_ID = 'replace-owner-submit-btn' type ReplaceOwnerProps = { onClose: () => void diff --git a/src/routes/safe/components/Settings/ManageOwners/dataFetcher.ts b/src/routes/safe/components/Settings/ManageOwners/dataFetcher.ts index 4f79229487..ea6337da32 100644 --- a/src/routes/safe/components/Settings/ManageOwners/dataFetcher.ts +++ b/src/routes/safe/components/Settings/ManageOwners/dataFetcher.ts @@ -3,9 +3,9 @@ import { List } from 'immutable' import { TableColumn } from 'src/components/Table/types.d' import { AddressBookState } from 'src/logic/addressBook/model/addressBook' -export const OWNERS_TABLE_NAME_ID = 'name' +const OWNERS_TABLE_NAME_ID = 'name' export const OWNERS_TABLE_ADDRESS_ID = 'address' -export const OWNERS_TABLE_ACTIONS_ID = 'actions' +const OWNERS_TABLE_ACTIONS_ID = 'actions' export type OwnerData = { address: string; name: string } diff --git a/src/routes/safe/components/Settings/SpendingLimit/InfoDisplay/DataDisplay.tsx b/src/routes/safe/components/Settings/SpendingLimit/InfoDisplay/DataDisplay.tsx deleted file mode 100644 index dcb74171b1..0000000000 --- a/src/routes/safe/components/Settings/SpendingLimit/InfoDisplay/DataDisplay.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { Text } from '@aura/safe-react-components' -import { ReactElement } from 'react' - -interface GenericInfoProps { - title?: string - children: React.ReactNode -} - -const DataDisplay = ({ title, children }: GenericInfoProps): ReactElement => ( - <> - {title && ( - - {title} - - )} - {children} - -) - -export default DataDisplay diff --git a/src/routes/safe/components/Settings/SpendingLimit/InfoDisplay/index.ts b/src/routes/safe/components/Settings/SpendingLimit/InfoDisplay/index.ts index 2d6b7eb13d..ca9c9f549c 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/InfoDisplay/index.ts +++ b/src/routes/safe/components/Settings/SpendingLimit/InfoDisplay/index.ts @@ -3,4 +3,3 @@ import ResetTimeInfo from './ResetTimeInfo' import TokenInfo from './TokenInfo' export { AddressInfo, ResetTimeInfo, TokenInfo } -export default './DataDisplay' diff --git a/src/routes/safe/components/Settings/SpendingLimit/LimitsTable/dataFetcher.ts b/src/routes/safe/components/Settings/SpendingLimit/LimitsTable/dataFetcher.ts index 11fbbe66eb..6887d8ebe2 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/LimitsTable/dataFetcher.ts +++ b/src/routes/safe/components/Settings/SpendingLimit/LimitsTable/dataFetcher.ts @@ -7,7 +7,7 @@ import { relativeTime } from 'src/utils/date' export const SPENDING_LIMIT_TABLE_BENEFICIARY_ID = 'beneficiary' export const SPENDING_LIMIT_TABLE_SPENT_ID = 'spent' export const SPENDING_LIMIT_TABLE_RESET_TIME_ID = 'resetTime' -export const SPENDING_LIMIT_TABLE_ACTION_ID = 'action' +const SPENDING_LIMIT_TABLE_ACTION_ID = 'action' export type SpendingLimitTable = { [SPENDING_LIMIT_TABLE_BENEFICIARY_ID]: string @@ -23,7 +23,7 @@ export type SpendingLimitTable = { } } -export const getSpendingLimitData = (spendingLimits?: SpendingLimitRow[] | null): SpendingLimitTable[] | undefined => +const getSpendingLimitData = (spendingLimits?: SpendingLimitRow[] | null): SpendingLimitTable[] | undefined => spendingLimits?.map((spendingLimit) => ({ [SPENDING_LIMIT_TABLE_BENEFICIARY_ID]: spendingLimit.delegate, [SPENDING_LIMIT_TABLE_SPENT_ID]: { diff --git a/src/routes/safe/components/Settings/SpendingLimit/LimitsTable/index.tsx b/src/routes/safe/components/Settings/SpendingLimit/LimitsTable/index.tsx deleted file mode 100644 index 521db0653f..0000000000 --- a/src/routes/safe/components/Settings/SpendingLimit/LimitsTable/index.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { Text, Icon } from '@aura/safe-react-components' -import TableContainer from '@material-ui/core/TableContainer' -import cn from 'classnames' -import { ReactElement, useState } from 'react' -import { useSelector } from 'react-redux' - -import ButtonHelper from 'src/components/ButtonHelper' -import Row from 'src/components/layout/Row' -import { TableCell, TableRow } from 'src/components/layout/Table' -import Table from 'src/components/Table' -import { AddressInfo } from 'src/routes/safe/components/Settings/SpendingLimit/InfoDisplay' -import { RemoveLimitModal } from 'src/routes/safe/components/Settings/SpendingLimit/RemoveLimitModal' -import { useStyles } from 'src/routes/safe/components/Settings/SpendingLimit/style' -import { grantedSelector } from 'src/utils/safeUtils/selector' - -import { - generateColumns, - SPENDING_LIMIT_TABLE_BENEFICIARY_ID, - SPENDING_LIMIT_TABLE_RESET_TIME_ID, - SPENDING_LIMIT_TABLE_SPENT_ID, - SpendingLimitTable, -} from './dataFetcher' -import { SpentVsAmount } from './SpentVsAmount' - -interface SpendingLimitTableProps { - data?: SpendingLimitTable[] -} - -export const LimitsTable = ({ data }: SpendingLimitTableProps): ReactElement => { - const classes = useStyles() - const granted = useSelector(grantedSelector) - - const columns = generateColumns() - const autoColumns = columns.filter(({ custom }) => !custom) - - const [selectedRow, setSelectedRow] = useState() - - return ( - <> - - - {(sortedData) => - sortedData.map((row, index) => ( - = 3 && index === sortedData.size - 1 && classes.noBorderBottom)} - data-testid="spending-limit-table-row" - key={index} - tabIndex={-1} - > - {autoColumns.map((column, index) => { - const columnId = column.id - const rowElement = row[columnId] - - return ( - - {columnId === SPENDING_LIMIT_TABLE_BENEFICIARY_ID && } - {columnId === SPENDING_LIMIT_TABLE_SPENT_ID && } - {columnId === SPENDING_LIMIT_TABLE_RESET_TIME_ID && ( - {rowElement.relativeTime} - )} - - ) - })} - - - {granted && ( - setSelectedRow(row)} data-testid="remove-limit-btn"> - - - )} - - - - )) - } -
-
- {selectedRow && ( - setSelectedRow(undefined)} spendingLimit={selectedRow} open={true} /> - )} - - ) -} diff --git a/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/index.tsx b/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/index.tsx index 390e364149..1e8887af37 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/index.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/index.tsx @@ -1,17 +1,13 @@ import { List } from 'immutable' -import { ReactElement, Reducer, useCallback, useReducer } from 'react' +import { Reducer, useCallback, useReducer } from 'react' import { useSelector } from 'react-redux' -import { Modal } from 'src/components/Modal' import { makeToken, Token } from 'src/logic/tokens/store/model/token' import { sameAddress } from 'src/logic/wallets/ethAddresses' import { extendedSafeTokensSelector } from 'src/utils/safeUtils/selector' -import Create from './Create' -import { ReviewSpendingLimits } from './Review' - export const CREATE = 'CREATE' as const -export const REVIEW = 'REVIEW' as const +const REVIEW = 'REVIEW' as const type Step = typeof CREATE | typeof REVIEW @@ -80,25 +76,3 @@ interface SpendingLimitModalProps { close: () => void open: boolean } - -export const NewLimitModal = ({ close, open }: SpendingLimitModalProps): ReactElement => { - // state and dispatch - const [{ step, txToken, values }, { create, review }] = useNewLimitModal(CREATE) - - const handleReview = async (values) => { - // if form is valid, we update the state to REVIEW and sets values - review({ step, txToken, values }) - } - - return ( - - {step === CREATE && } - {step === REVIEW && } - - ) -} diff --git a/src/routes/safe/components/Settings/SpendingLimit/NewLimitSteps.tsx b/src/routes/safe/components/Settings/SpendingLimit/NewLimitSteps.tsx deleted file mode 100644 index 48726f9f21..0000000000 --- a/src/routes/safe/components/Settings/SpendingLimit/NewLimitSteps.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { Text } from '@aura/safe-react-components' -import { ReactElement } from 'react' -import styled from 'styled-components' - -import Img from 'src/components/layout/Img' -import AssetAmount from './assets/asset-amount.svg' -import Beneficiary from './assets/beneficiary.svg' -import Time from './assets/time.svg' - -const StepWrapper = styled.div` - display: flex; - justify-content: space-around; - margin-top: 20px; - max-width: 720px; - text-align: center; -` - -const Step = styled.div` - width: 24%; - min-width: 120px; - max-width: 164px; -` - -const StepsLine = styled.div` - height: 2px; - flex: 1; - background: #d4d5d3; - margin: 46px 0; -` - -export const NewLimitSteps = (): ReactElement => ( - - - Select Beneficiary - - - Select Beneficiary - - - - Define beneficiary that will be able to use the allowance. - - - - The beneficiary does not have to be an owner of this Safe - - - - - - - Select asset and amount - - - Select asset and amount - - - - You can set a spending limit for any asset stored in your Safe - - - - - - - Select time - - - Select time - - - - You can choose to set a one-time spending limit or to have it automatically refill after a defined time-period - - - -) diff --git a/src/routes/safe/components/Settings/SpendingLimit/index.tsx b/src/routes/safe/components/Settings/SpendingLimit/index.tsx deleted file mode 100644 index e950adb577..0000000000 --- a/src/routes/safe/components/Settings/SpendingLimit/index.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { Button, Text, Title } from '@aura/safe-react-components' -import { ReactElement, useState } from 'react' -import { useSelector } from 'react-redux' -import styled from 'styled-components' - -import Block from 'src/components/layout/Block' -import Col from 'src/components/layout/Col' -import Row from 'src/components/layout/Row' -import { currentSafeSpendingLimits } from 'src/logic/safe/store/selectors' -import { grantedSelector } from 'src/utils/safeUtils/selector' - -import { LimitsTable } from './LimitsTable' -import { getSpendingLimitData } from './LimitsTable/dataFetcher' -import { NewLimitModal } from './NewLimitModal' -import { NewLimitSteps } from './NewLimitSteps' -import { useStyles } from './style' - -const InfoText = styled(Text)` - margin-top: 16px; -` - -const SpendingLimit = (): ReactElement => { - const classes = useStyles() - const granted = useSelector(grantedSelector) - const allowances = useSelector(currentSafeSpendingLimits) - const spendingLimitData = getSpendingLimitData(allowances) - - const [showNewSpendingLimitModal, setShowNewSpendingLimitModal] = useState(false) - const openNewSpendingLimitModal = () => { - setShowNewSpendingLimitModal(true) - } - const closeNewSpendingLimitModal = () => { - setShowNewSpendingLimitModal(false) - } - - return ( - <> - - - Spending limit - - - You can set rules for specific beneficiaries to access funds from this Safe without having to collect all - signatures. - - {spendingLimitData?.length ? : } - - - {granted && ( - <> - - - - - - {showNewSpendingLimitModal && } - - )} - - ) -} - -export default SpendingLimit diff --git a/src/routes/safe/components/Settings/ThresholdSettings/ChangeThreshold/index.tsx b/src/routes/safe/components/Settings/ThresholdSettings/ChangeThreshold/index.tsx deleted file mode 100644 index 8529bbe186..0000000000 --- a/src/routes/safe/components/Settings/ThresholdSettings/ChangeThreshold/index.tsx +++ /dev/null @@ -1,215 +0,0 @@ -import MenuItem from '@material-ui/core/MenuItem' -import { ReactElement, useEffect, useState } from 'react' -import { useDispatch, useSelector } from 'react-redux' - -import Field from 'src/components/forms/Field' -import GnoForm from 'src/components/forms/GnoForm' -import SelectField from 'src/components/forms/SelectField' -import { composeValidators, differentFrom, minValue, mustBeInteger, required } from 'src/components/forms/validator' -import Block from 'src/components/layout/Block' -import Col from 'src/components/layout/Col' -import Hairline from 'src/components/layout/Hairline' -import Paragraph from 'src/components/layout/Paragraph' -import Row from 'src/components/layout/Row' -import { ModalHeader } from 'src/routes/safe/components/Balances/SendModal/screens/ModalHeader' -import { EditableTxParameters } from 'src/utils/transactionHelpers/EditableTxParameters' -import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters' -import { currentSafeCurrentVersion } from 'src/logic/safe/store/selectors' -import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts' -import { useEstimationStatus } from 'src/logic/hooks/useEstimationStatus' -import { EstimationStatus, useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas' -import { Modal } from 'src/components/Modal' -import { ButtonStatus } from 'src/components/Modal/type' -import { ReviewInfoText } from 'src/components/ReviewInfoText' -import { TxParametersDetail } from 'src/utils/transactionHelpers/TxParametersDetail' -import { createTransaction } from 'src/logic/safe/store/actions/createTransaction' -import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions' - -import { useStyles } from './style' - -const THRESHOLD_FIELD_NAME = 'threshold' - -type ChangeThresholdModalProps = { - onClose: () => void - ownersCount?: number - safeAddress: string - threshold?: number -} - -export const ChangeThresholdModal = ({ - onClose, - ownersCount = 0, - safeAddress, - threshold = 1, -}: ChangeThresholdModalProps): ReactElement => { - const classes = useStyles() - const dispatch = useDispatch() - const safeVersion = useSelector(currentSafeCurrentVersion) as string - const [data, setData] = useState('') - const [manualSafeTxGas, setManualSafeTxGas] = useState('0') - const [manualGasPrice, setManualGasPrice] = useState() - const [manualGasLimit, setManualGasLimit] = useState() - const [editedThreshold, setEditedThreshold] = useState(threshold) - const [disabledSubmitForm, setDisabledSubmitForm] = useState(true) - - const { - gasCostFormatted, - txEstimationExecutionStatus, - isCreation, - isExecution, - isOffChainSignature, - gasLimit, - gasPriceFormatted, - gasEstimation, - } = useEstimateTransactionGas({ - txData: data, - txRecipient: safeAddress, - safeTxGas: manualSafeTxGas, - manualGasPrice, - manualGasLimit, - }) - - const [buttonStatus] = useEstimationStatus(txEstimationExecutionStatus) - - useEffect(() => { - let isCurrent = true - const calculateChangeThresholdData = () => { - const safeInstance = getGnosisSafeInstanceAt(safeAddress, safeVersion) - const txData = safeInstance.methods.changeThreshold(editedThreshold).encodeABI() - if (isCurrent) { - setData(txData) - } - } - - calculateChangeThresholdData() - return () => { - isCurrent = false - } - }, [safeAddress, safeVersion, editedThreshold]) - - const handleThreshold = ({ target }) => { - const value = parseInt(target.value) - setDisabledSubmitForm(value === editedThreshold || value === threshold) - setEditedThreshold(value) - } - - const handleSubmit = ({ txParameters }) => { - dispatch( - createTransaction({ - safeAddress, - to: safeAddress, - valueInWei: '0', - txData: data, - txNonce: txParameters.safeNonce, - safeTxGas: txParameters.safeTxGas, - ethParameters: txParameters, - notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX, - }), - ) - onClose() - } - - const closeEditModalCallback = (txParameters: TxParameters) => { - const oldGasPrice = gasPriceFormatted - const newGasPrice = txParameters.ethGasPrice - const oldSafeTxGas = gasEstimation - const newSafeTxGas = txParameters.safeTxGas - - if (newGasPrice && oldGasPrice !== newGasPrice) { - setManualGasPrice(txParameters.ethGasPrice) - } - - if (txParameters.ethGasLimit && gasLimit !== txParameters.ethGasLimit) { - setManualGasLimit(txParameters.ethGasLimit) - } - - if (newSafeTxGas && oldSafeTxGas !== newSafeTxGas) { - setManualSafeTxGas(newSafeTxGas) - } - } - - return ( - - {(txParameters, toggleEditMode) => ( - <> - - - - {() => ( - <> - - - Any transaction requires the confirmation of: - - - - ( - <> - - {[...Array(Number(ownersCount))].map((x, index) => ( - - {index + 1} - - ))} - - - )} - validate={composeValidators(required, mustBeInteger, minValue(1), differentFrom(threshold))} - /> - - - - {`out of ${ownersCount} owner(s)`} - - - - - {/* Tx Parameters */} - - - {txEstimationExecutionStatus !== EstimationStatus.LOADING && ( - - )} - - - - - - )} - - - )} - - ) -} diff --git a/src/routes/safe/components/Settings/ThresholdSettings/ChangeThreshold/style.ts b/src/routes/safe/components/Settings/ThresholdSettings/ChangeThreshold/style.ts deleted file mode 100644 index 11fdf32655..0000000000 --- a/src/routes/safe/components/Settings/ThresholdSettings/ChangeThreshold/style.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { createStyles, makeStyles } from '@material-ui/core' - -import { lg, md, sm } from 'src/theme/variables' - -export const useStyles = makeStyles( - createStyles({ - modalContent: { - padding: `${md} ${lg}`, - }, - ownersText: { - marginLeft: sm, - }, - inputRow: { - position: 'relative', - }, - }), -) diff --git a/src/routes/safe/components/Settings/assets/icons/OwnersIcon.tsx b/src/routes/safe/components/Settings/assets/icons/OwnersIcon.tsx deleted file mode 100644 index 25a3728316..0000000000 --- a/src/routes/safe/components/Settings/assets/icons/OwnersIcon.tsx +++ /dev/null @@ -1,11 +0,0 @@ -export const OwnersIcon = (): React.ReactElement => ( - - - - - - -) diff --git a/src/routes/safe/components/Settings/assets/icons/RequiredConfirmationsIcon.tsx b/src/routes/safe/components/Settings/assets/icons/RequiredConfirmationsIcon.tsx deleted file mode 100644 index d77b956a50..0000000000 --- a/src/routes/safe/components/Settings/assets/icons/RequiredConfirmationsIcon.tsx +++ /dev/null @@ -1,11 +0,0 @@ -export const RequiredConfirmationsIcon = (): React.ReactElement => ( - - - - - - -) diff --git a/src/routes/safe/components/Settings/assets/icons/SafeDetailsIcon.tsx b/src/routes/safe/components/Settings/assets/icons/SafeDetailsIcon.tsx deleted file mode 100644 index 8b918b1d57..0000000000 --- a/src/routes/safe/components/Settings/assets/icons/SafeDetailsIcon.tsx +++ /dev/null @@ -1,14 +0,0 @@ -export const SafeDetailsIcon = (): React.ReactElement => ( - - - - - - - - -) diff --git a/src/routes/safe/components/assets/AddressBookIcon.tsx b/src/routes/safe/components/assets/AddressBookIcon.tsx deleted file mode 100644 index 97915f50be..0000000000 --- a/src/routes/safe/components/assets/AddressBookIcon.tsx +++ /dev/null @@ -1,12 +0,0 @@ -export const AddressBookIcon = (): React.ReactElement => ( - - - - - - -) diff --git a/src/routes/safe/components/assets/BalancesIcon.tsx b/src/routes/safe/components/assets/BalancesIcon.tsx deleted file mode 100644 index 1f3796d91c..0000000000 --- a/src/routes/safe/components/assets/BalancesIcon.tsx +++ /dev/null @@ -1,16 +0,0 @@ -export const BalancesIcon = (): React.ReactElement => ( - - - - - - - -) diff --git a/src/routes/safe/components/assets/SettingsIcon.tsx b/src/routes/safe/components/assets/SettingsIcon.tsx deleted file mode 100644 index 4908930c03..0000000000 --- a/src/routes/safe/components/assets/SettingsIcon.tsx +++ /dev/null @@ -1,11 +0,0 @@ -export const SettingsIcon = (): React.ReactElement => ( - - - - - - -) diff --git a/src/routes/safe/components/assets/TransactionsIcon.tsx b/src/routes/safe/components/assets/TransactionsIcon.tsx deleted file mode 100644 index 8e2ec3c777..0000000000 --- a/src/routes/safe/components/assets/TransactionsIcon.tsx +++ /dev/null @@ -1,11 +0,0 @@ -export const TransactionsIcon = (): React.ReactElement => ( - - - - - - -) diff --git a/src/routes/safe/container/hooks/useTransactionFee.ts b/src/routes/safe/container/hooks/useTransactionFee.ts deleted file mode 100644 index 9cf32bd6fe..0000000000 --- a/src/routes/safe/container/hooks/useTransactionFee.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { calculateFee, GasPrice, SignerData, StdFee } from '@cosmjs/stargate' -import { ChainInfo } from '@gnosis.pm/safe-react-gateway-sdk' -import { useEffect, useState } from 'react' -import { getInternalChainId } from 'src/config' -import { EstimationStatus } from 'src/logic/hooks/useEstimateTransactionGas' -import { getAccountOnChain, getMChainsConfig } from 'src/services' -import { ReviewTxProp } from '../../components/Balances/SendModal/screens/ReviewSendFundsTx' - -export type TxFee = { - sendFee: StdFee | undefined - gasEstimation: number | undefined - gasPrice: GasPrice | undefined - signerData: SignerData | undefined - gasPriceFormatted: string | undefined - txEstimationStatus: EstimationStatus -} -/** - * This hooks is used to store tx parameter - * It needs to be initialized calling setGasEstimation. - */ -export const useTransactionFees = ( - chainInfo: ChainInfo, - tx: ReviewTxProp, - safeAddress: string, - gasDefault: string | undefined, - manualGasLimit: string | undefined, -): TxFee => { - const [gasEstimation, setGasEstimation] = useState() - const [gasPrice, setGasPrice] = useState() - const [gasPriceFormatted, setGasPriceFormatted] = useState() - const [sendFee, setSendFee] = useState() - const [signerData, setSignerData] = useState() - const [txEstimationStatus, setTxEstimationStatus] = useState(EstimationStatus.LOADING) - - const { chainId, rpcUri, shortName } = chainInfo - - useEffect(() => { - const loadFee = async () => { - const listChain = await getMChainsConfig() - - const chainInfo = listChain.find((x) => x.chainId === chainId) - - const denom = chainInfo?.denom || '' - - if (window.getOfflineSignerOnlyAmino) { - // const offlineSigner = window.getOfflineSignerOnlyAmino(chainId) - - // const amountFinal = - // shortName === 'evmos' - // ? Math.floor(Number(tx?.amount) * Math.pow(10, 18)).toString() || '' - // : Math.floor(Number(tx?.amount) * Math.pow(10, 6)).toString() || '' - - const signingInstruction = await (async () => { - // get account on chain from API - const { Data: accountOnChainResult } = await getAccountOnChain(safeAddress, getInternalChainId()) - // const accountOnChain = await client.getAccount(safeAddress) - - return { - accountNumber: accountOnChainResult?.accountNumber, - sequence: accountOnChainResult?.sequence, - memo: '', - } - })() - - // const msgSend: Partial = { - // fromAddress: safeAddress, - // toAddress: tx.recipientAddress, - // amount: coins(amountFinal, denom), - // } - - // const msg: MsgSendEncodeObject = { - // typeUrl: '/cosmos.bank.v1beta1.MsgSend', - // value: msgSend, - // } - - const defaultGas = chainInfo?.defaultGas.find((gas) => gas.typeUrl === '/cosmos.bank.v1beta1.MsgSend') - - const gasEstimation = defaultGas?.gasAmount || '400000' - - // "/cosmos.bank.v1beta1.MsgSend" - - // const accounts = await offlineSigner.getAccounts() - - // const onlineClient = await SigningCosmWasmClient.connectWithSigner(rpcUri.value, offlineSigner) - - // const gasEstimation = await onlineClient.simulate(accounts[0].address, [msg], signingInstruction.memo) - - // const gasEstimation = await Promise.all([ - // offlineSigner.getAccounts(), - // SigningCosmWasmClient.connectWithSigner(rpcUri.value, offlineSigner), - // ]) - // .then(([accounts, onlineClient]) => - // onlineClient.simulate(accounts[0].address, [msg], signingInstruction.memo), - // ) - // .then((gasEstimation) => { - // gasEstimation && setTxEstimationStatus(EstimationStatus.SUCCESS) - - // return gasEstimation - // }) - // .catch((error) => { - // console.log({ error }) - - // setTxEstimationStatus(EstimationStatus.FAILURE) - - // return 80000 - // }) - - // const multiplier = 1.3 - const gasPrice = GasPrice.fromString(`${chainInfo?.defaultGasPrice || gasDefault}${denom}`) - const sendFee = calculateFee(Math.round(+gasEstimation), gasPrice) - - const signerData: SignerData = { - accountNumber: signingInstruction.accountNumber || 0, - sequence: signingInstruction.sequence || 0, - chainId: chainId, - } - - const _gasPrice = +sendFee.amount[0].amount / Math.pow(10, 6) - - gasEstimation && setTxEstimationStatus(EstimationStatus.SUCCESS) - - setSignerData(signerData) - setGasPrice(gasPrice) - setGasPriceFormatted(_gasPrice.toString()) - setGasEstimation(manualGasLimit ? +manualGasLimit : +gasEstimation) - setSendFee(sendFee) - } - } - - loadFee() - }, [chainId, rpcUri, shortName, tx, safeAddress, gasDefault, manualGasLimit]) - - return { - sendFee, - gasEstimation, - gasPrice, - gasPriceFormatted, - signerData, - txEstimationStatus, - } -} diff --git a/src/services/constant/common.ts b/src/services/constant/common.ts index d6e404de72..3843c84b69 100644 --- a/src/services/constant/common.ts +++ b/src/services/constant/common.ts @@ -2,7 +2,7 @@ export const DEFAULT_PAGE_SIZE = 50 export const QUEUED_PAGE_SIZE = 1000 export const DEFAULT_PAGE_FIRST = 1 -export const DEFAULT_GAS = '0.00025' + export const DEFAULT_GAS_LIMIT = 400000 export const JWT_TOKEN_KEY = 'TOKEN' diff --git a/src/services/index.ts b/src/services/index.ts index a8a0f26dc7..b64e7a0a82 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -27,12 +27,7 @@ export interface ISafeCreate { export interface ISafeCancel { myAddress: string } -export interface ISafeAllow { - safeId: string - myAddress: string -} - -export interface IResponse { +interface IResponse { AdditionalData: any[] Data: any ErrorCode: string @@ -179,10 +174,6 @@ export function createSafeTransaction(transactionInfo: ICreateSafeTransaction): return axios.post(`${baseUrl}/transaction/create`, transactionInfo).then((res) => res.data) } -export function signSafeTransaction(transactionInfo: ISignSafeTransaction): Promise> { - return axios.post(`${baseUrl}/transaction/sign`, transactionInfo).then((res) => res.data) -} - export const getTxDetailById = async ( safeAddress: string, txId?: string, @@ -272,10 +263,6 @@ export async function getNumberOfDelegator(validatorId: any): Promise res.data) } -export function clamRewards(internalChainId: any, delegatorAddress: any): Promise> { - return axios.get(`${baseUrl}/general/${internalChainId}/${delegatorAddress}/rewards`).then((res) => res.data) -} - //VOTING export async function getProposals(internalChainId: number | string): Promise> { diff --git a/src/test/utils/safeHelper.ts b/src/test/utils/safeHelper.ts deleted file mode 100644 index 97413e0c5c..0000000000 --- a/src/test/utils/safeHelper.ts +++ /dev/null @@ -1,108 +0,0 @@ -//@ts-nocheck -import { NonPayableTransactionObject } from 'src/types/contracts/types.d' -import { PromiEvent } from 'web3-core' -import { GnosisSafe } from 'src/types/contracts/gnosis_safe.d' -import { ContractOptions, ContractSendMethod, DeployOptions, EventData, PastEventOptions } from 'web3-eth-contract' -import { LocalTransactionStatus, Transaction } from 'src/logic/safe/store/models/types/gateway.d' -import { TransferDirection, TokenType } from '@gnosis.pm/safe-react-gateway-sdk' - -const mockNonPayableTransactionObject = (callResult?: string): NonPayableTransactionObject => { - return { - arguments: [], - call: (tx?) => new Promise((resolve) => resolve(callResult || '')), - encodeABI: (tx?) => '', - estimateGas: (tx?) => new Promise((resolve) => resolve(1000)), - send: () => { return {} as PromiEvent} - } -} - -type SafeMethodsProps = { - threshold?: string - nonce?: string - isOwnerUserAddress?: string, - name?: string, - version?: string -} - -export const getMockedSafeInstance = (safeProps: SafeMethodsProps): GnosisSafe => { - const { threshold = '1', nonce = '0', isOwnerUserAddress, name = 'safeName', version = '1.0.0' } = safeProps - return { - defaultAccount: undefined, - defaultBlock: undefined, - defaultChain: undefined, - defaultCommon: undefined, - defaultHardfork: undefined, - handleRevert: false, - options: undefined, - transactionBlockTimeout: 0, - transactionConfirmationBlocks: 0, - transactionPollingTimeout: 0, - clone(): GnosisSafe { - return undefined; - }, - constructor(jsonInterface: any[], address?: string, options?: ContractOptions): GnosisSafe { - return undefined; - }, - deploy(options: DeployOptions): ContractSendMethod { - return undefined; - }, - getPastEvents(event: string, options?: PastEventOptions | ((error: Error, event: EventData) => void), callback?: (error: Error, event: EventData) => void): Promise { - return undefined; - }, - once(event: "AddedOwner" | "ExecutionFromModuleSuccess" | "EnabledModule" | "ChangedMasterCopy" | "ExecutionFromModuleFailure" | "RemovedOwner" | "ApproveHash" | "DisabledModule" | "SignMsg" | "ExecutionSuccess" | "ChangedThreshold" | "ExecutionFailure", cb: any): void { - }, - events: { } as any, - methods: { - NAME: (): NonPayableTransactionObject => mockNonPayableTransactionObject(name) as NonPayableTransactionObject, - VERSION: (): NonPayableTransactionObject => mockNonPayableTransactionObject(version) as NonPayableTransactionObject, - addOwnerWithThreshold: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, - approvedHashes: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, - changeMasterCopy: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, - changeThreshold: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, - disableModule: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, - domainSeparator: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, - enableModule: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, - execTransactionFromModule: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, - execTransactionFromModuleReturnData: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, - getModules: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, - getThreshold: (): NonPayableTransactionObject => mockNonPayableTransactionObject(threshold) as NonPayableTransactionObject, - isOwner: (): NonPayableTransactionObject => mockNonPayableTransactionObject(isOwnerUserAddress) as NonPayableTransactionObject, - nonce: (): NonPayableTransactionObject => mockNonPayableTransactionObject(nonce) as NonPayableTransactionObject, - removeOwner: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, - setFallbackHandler: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, - signedMessages: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, - swapOwner: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, - setup: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, - execTransaction: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, - requiredTxGas: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, - approveHash: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, - signMessage: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, - isValidSignature: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, - getMessageHash: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, - encodeTransactionData: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, - getTransactionHash: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, - } as any - } -} - -export const getMockedStoredTServiceModel = (txProps?: Transaction): Transaction => ({ - id: "multisig_123", - timestamp: 123456, - txStatus: LocalTransactionStatus.PENDING, - txInfo: { - type: 'Transfer', - sender: { value: "0x123", name: null, logoUri: null }, - recipient: { value: "0x456", name: null, logoUri: null }, - direction: TransferDirection.OUTGOING, - transferInfo: { - type: TokenType.ERC20, - tokenAddress: "0xabc", - tokenName: null, - tokenSymbol: null, - logoUri: null, - decimals: 18, - value: "1000000000000000000", - } - }, - ...txProps -} as Transaction) diff --git a/src/types/proposal.ts b/src/types/proposal.ts index 7b19957384..29d37fcb09 100644 --- a/src/types/proposal.ts +++ b/src/types/proposal.ts @@ -6,8 +6,6 @@ export enum ProposalStatus { Failed = 'PROPOSAL_STATUS_FAILED', } -export type VoteLabel = 'Yes' | 'No' | 'Abstain' | 'NoWithVeto' -export type VoteKey = 'yes' | 'abstain' | 'no' | 'no_with_veto' export enum VoteMapping { 'yes' = 'Yes', @@ -16,7 +14,7 @@ export enum VoteMapping { 'no_with_veto' = 'NoWithVeto', } -export interface VoteResult { +interface VoteResult { number?: string name?: string percent: string diff --git a/src/utils/calc.ts b/src/utils/calc.ts index d623cacf56..313cbb3745 100644 --- a/src/utils/calc.ts +++ b/src/utils/calc.ts @@ -1,64 +1,7 @@ import BigNumber from 'bignumber.js' -import { VoteMapping } from 'src/types/proposal' - -function calcPercent(total: number, value: number): number { - return (value * 100) / total || 0 -} - -function calcPercentInObj(obj: { [key: string]: any }): { [key: string]: number } { - const keys = Object.keys(obj) - - const total = keys.reduce((total, key) => { - return (total += Number(obj[key])) - }, 0) - - const value = {} - - keys.forEach((key) => { - Object.assign(value, { - [key]: calcPercent(total, Number(obj[key])), - }) - }) - - return value -} - -function calcVotePercent(obj: any): any { - const keys = Object.keys(obj) - const value = {} - keys.forEach((key) => { - Object.assign(value, { - [VoteMapping[key]]: obj[key], - }) - }) - - return value -} - -function maxVote(percent) { - let max: { [x: string]: any } | null = null - Object.keys(percent).forEach((key) => { - if (!max) { - max = { - key: key, - value: `${(+percent[key]).toFixed(2)}%`, - } - return - } - - if (max[key] < percent[key]) { - max = { - key: key, - value: `${(+percent[key]).toFixed(2)}%`, - } - } - }) - - return max -} function calcBalance(amount: string, decimals: number): string { return new BigNumber(amount).dividedBy(Math.pow(10, decimals)).toString() } -export { calcPercent, calcPercentInObj, calcVotePercent, maxVote, calcBalance } +export { calcBalance } diff --git a/src/utils/clipboard.ts b/src/utils/clipboard.ts deleted file mode 100644 index d540a71cca..0000000000 --- a/src/utils/clipboard.ts +++ /dev/null @@ -1,15 +0,0 @@ -export const copyToClipboard = (text: string): void => { - const range = document.createRange() - range.selectNodeContents(document.body) - document?.getSelection()?.addRange(range) - - function listener(e: ClipboardEvent) { - e.clipboardData?.setData('text/plain', text) - e.preventDefault() - } - document.addEventListener('copy', listener) - document.execCommand('copy') - document.removeEventListener('copy', listener) - - document?.getSelection()?.removeAllRanges() -} diff --git a/src/utils/css.ts b/src/utils/css.ts index 8e28660eb3..c644d2d0fd 100644 --- a/src/utils/css.ts +++ b/src/utils/css.ts @@ -1,4 +1,4 @@ -export const upperFirst = (value: string): string => value.charAt(0).toUpperCase() + value.toLowerCase().slice(1) +const upperFirst = (value: string): string => value.charAt(0).toUpperCase() + value.toLowerCase().slice(1) export const capitalize = (value: string, prefix?: string): undefined | boolean | string | number => { if (!value) { diff --git a/src/utils/date.ts b/src/utils/date.ts index 39d0e8c56d..7e83f83b03 100644 --- a/src/utils/date.ts +++ b/src/utils/date.ts @@ -29,7 +29,7 @@ export const getLocalStartOfDate = (timestamp: number): number => { export const formatWithSchema = (timestamp: number, schema: string): string => timestamp ? format(timestamp, schema) : 'Invalid time' -export const formatTime = (timestamp: number): string => formatWithSchema(timestamp, 'h:mm a') +const formatTime = (timestamp: number): string => formatWithSchema(timestamp, 'h:mm a') export const formatDateTime = (timestamp: number): string => formatWithSchema(timestamp, 'MMM d, yyyy - h:mm:ss a') diff --git a/src/utils/hooks/useKnownAddress.ts b/src/utils/hooks/useKnownAddress.ts deleted file mode 100644 index 4e1e4fc091..0000000000 --- a/src/utils/hooks/useKnownAddress.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { useSelector } from 'react-redux' - -import { sameString } from 'src/utils/strings' -import { ADDRESS_BOOK_DEFAULT_NAME } from 'src/logic/addressBook/model/addressBook' -import { addressBookEntryName } from 'src/logic/addressBook/store/selectors' -import { AddressEx } from '@gnosis.pm/safe-react-gateway-sdk' - -const DEFAULT_PROPS: AddressEx = { - value: '', - name: null, - logoUri: null, -} -export const useKnownAddress = (props: AddressEx | null = DEFAULT_PROPS): AddressEx & { isInAddressBook: boolean } => { - const recipientName = useSelector((state) => addressBookEntryName(state, { address: props?.value || '' })) - - // Undefined known address - if (!props) { - return { - ...DEFAULT_PROPS, - isInAddressBook: false, - } - } - - // We have to check that the name returned is not UNKNOWN - const isInAddressBook = !sameString(recipientName, ADDRESS_BOOK_DEFAULT_NAME) - const name = isInAddressBook && recipientName ? recipientName : props?.name - - return { - ...props, - name, - isInAddressBook, - } -} diff --git a/src/utils/hooks/usePagedHistoryTransactions.ts b/src/utils/hooks/usePagedHistoryTransactions.ts index a93f7b6ba4..a2af463a2a 100644 --- a/src/utils/hooks/usePagedHistoryTransactions.ts +++ b/src/utils/hooks/usePagedHistoryTransactions.ts @@ -70,7 +70,7 @@ export const usePagedHistoryTransactions = (): PagedTransactions => { return { count, transactions, hasMore, next, isLoading } } -export const useHistoryTransactions = (): TransactionDetails => { +const useHistoryTransactions = (): TransactionDetails => { const historyTxs = useSelector(historyTransactions) const [count, setCount] = useState(0) diff --git a/src/utils/hooks/usePagedQueuedTransactions.ts b/src/utils/hooks/usePagedQueuedTransactions.ts index 423519b40a..79d64dbd2f 100644 --- a/src/utils/hooks/usePagedQueuedTransactions.ts +++ b/src/utils/hooks/usePagedQueuedTransactions.ts @@ -73,7 +73,7 @@ export const usePagedQueuedTransactions = (): PagedQueuedTransactions => { /** * Get transactions (next and queue) from nextTransactions and queuedTransactions selectors */ -export const useQueueTransactions = (): TransactionDetails => { +const useQueueTransactions = (): TransactionDetails => { const nextTxs = useSelector(nextTransactions) const queuedTxs = useSelector(queuedTransactions) const allTxs = useSelector(txsTransactions) diff --git a/src/utils/hooks/useTransactionDetails.ts b/src/utils/hooks/useTransactionDetails.ts index 85901b8bab..afc73c6e34 100644 --- a/src/utils/hooks/useTransactionDetails.ts +++ b/src/utils/hooks/useTransactionDetails.ts @@ -4,7 +4,7 @@ import { fetchTransactionDetailsById } from 'src/logic/safe/store/actions/fetchT import { getTransactionByAttribute } from 'src/logic/safe/store/selectors/gatewayTransactions' import { AppReduxState } from 'src/logic/safe/store' -export type LoadTransactionDetails = { +type LoadTransactionDetails = { data?: any loading: boolean } diff --git a/src/utils/hooks/useTransactionStatus.ts b/src/utils/hooks/useTransactionStatus.ts index fc0b1ede24..de3d827997 100644 --- a/src/utils/hooks/useTransactionStatus.ts +++ b/src/utils/hooks/useTransactionStatus.ts @@ -8,7 +8,7 @@ import { LocalTransactionStatus, Transaction } from 'src/logic/safe/store/models import { userAccountSelector } from 'src/logic/wallets/store/selectors' import { addressInList } from 'src/utils/transactionUtils' -export type TransactionStatusProps = { +type TransactionStatusProps = { color: string text: string } diff --git a/src/utils/index.ts b/src/utils/index.ts index 6816f73a37..c00334b2f1 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,11 +1,10 @@ -import { getChains } from 'src/config/cache/chains' -import { getChainDefaultGasPrice, getChainInfo, getCoinDecimal, getNativeCurrency } from 'src/config' -import { MChainInfo } from 'src/services' -import { calculateFee, GasPrice } from '@cosmjs/stargate' +import { GasPrice, calculateFee } from '@cosmjs/stargate' import BigNumber from 'bignumber.js' -import calculateGasFee from 'src/logic/providers/utils/fee' -import { useLocation } from 'react-router-dom' import { useMemo } from 'react' +import { useLocation } from 'react-router-dom' +import { getChainDefaultGasPrice, getChainInfo, getNativeCurrency } from 'src/config' +import { getChains } from 'src/config/cache/chains' +import { MChainInfo } from 'src/services' export const beutifyJson = (data) => { if (!data) return '' @@ -31,9 +30,7 @@ export const beutifyJson = (data) => { ) return formattedJson } -export const validateFloatNumber = (value: any): boolean => { - return !isNaN(parseFloat(value)) && isFinite(value) -} + export const formatNumber = (value: any): string => { const nativeCurrency = getNativeCurrency() @@ -58,12 +55,7 @@ export const isNumberKeyPress = (event): boolean => { } return true } -export const roundGasAmount = (amount) => { - const decimal = getCoinDecimal() - const chainDefaultGasPrice = getChainDefaultGasPrice() - const fee = calculateGasFee(+amount, +chainDefaultGasPrice, decimal) - return amount -} + export const formatBigNumber = (amount, isMulti = false) => { const nativeCurrency = getNativeCurrency() if (isNaN(amount)) return '0' @@ -90,7 +82,7 @@ export const formatNativeCurrency = (amount) => { Number(new BigNumber(new BigNumber(+amount).toFixed(+nativeCurrency.decimals)).toFixed()), )} ${nativeCurrency.symbol}` } -export const formatWithComma = (amount): string => { +const formatWithComma = (amount): string => { if (+amount > 1) { const intl = new Intl.NumberFormat('en-US') return intl.format(amount) diff --git a/src/utils/intercom.ts b/src/utils/intercom.ts index 957d88e71e..ef7dcca938 100644 --- a/src/utils/intercom.ts +++ b/src/utils/intercom.ts @@ -2,7 +2,6 @@ import crypto from 'crypto' import { CookieAttributes } from 'js-cookie' import { COOKIES_KEY_INTERCOM } from 'src/logic/cookies/model/cookie' import { loadFromCookie, saveCookie } from 'src/logic/cookies/utils' -import { INTERCOM_ID } from 'src/utils/constants' let intercomLoaded = false @@ -23,31 +22,6 @@ const getIntercomUserId = async () => { return userId } -// eslint-disable-next-line consistent-return -export const loadIntercom = async (): Promise => { - const APP_ID = INTERCOM_ID - if (!APP_ID) { - console.error('[Intercom] - In order to use Intercom you need to add an appID') - return - } - const d = document - const s = d.createElement('script') - s.type = 'text/javascript' - s.async = true - s.src = `https://widget.intercom.io/widget/${APP_ID}` - const x = d.getElementsByTagName('script')[0] - x?.parentNode?.insertBefore(s, x) - - const intercomUserId = await getIntercomUserId() - - s.onload = () => { - ;(window as any).Intercom('boot', { - app_id: APP_ID, - user_id: intercomUserId, - }) - intercomLoaded = true - } -} export const closeIntercom = (): void => { if (!isIntercomLoaded()) return diff --git a/src/utils/signer.ts b/src/utils/signer.ts index 039f5ce2f8..dd4c057a1b 100644 --- a/src/utils/signer.ts +++ b/src/utils/signer.ts @@ -326,7 +326,7 @@ const getDefaultOptions = (): KeplrIntereactionOptions => ({ }, }) -export const signMessage = async ( +const signMessage = async ( chainId: string, safeAddress: string, messages: any, diff --git a/src/utils/transactionUtils.ts b/src/utils/transactionUtils.ts index deebe030fc..f475c387ba 100644 --- a/src/utils/transactionUtils.ts +++ b/src/utils/transactionUtils.ts @@ -1,149 +1,22 @@ import { AddressEx, - TransactionInfo, - Transfer, TokenType, - TransactionDetails, - MultisigExecutionDetails, - MultisigExecutionInfo, TransactionListPage, - TransferDirection, - TransactionListItem, TransactionStatus, - TransactionSummary, + TransferDirection, } from '@gnosis.pm/safe-react-gateway-sdk' -import { BigNumber } from 'bignumber.js' import { matchPath } from 'react-router-dom' -import { getNativeCurrency } from 'src/config' -import { getNativeCurrencyAddress } from 'src/config/utils' -import { - isCustomTxInfo, - isModuleExecutionInfo, - isMultiSigExecutionDetails, - isTransferTxInfo, - isTxQueued, - LocalTransactionStatus, - Transaction, -} from 'src/logic/safe/store/models/types/gateway.d' -import { formatAmount } from 'src/logic/tokens/utils/formatAmount' -import { sameAddress, ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses' -import { SAFE_ROUTES, TRANSACTION_ID_SLUG, history, extractSafeAddress } from 'src/routes/routes' +import { sameAddress } from 'src/logic/wallets/ethAddresses' +import { history, SAFE_ROUTES, TRANSACTION_ID_SLUG } from 'src/routes/routes' import { DEFAULT_PAGE_FIRST, DEFAULT_PAGE_SIZE } from 'src/services/constant/common' import { ITransactionListItem, ITransactionListQuery, MTransactionListItem } from 'src/types/transaction' -export const NOT_AVAILABLE = 'n/a' - -const inQueuedStatus = [ - TransactionStatus.PENDING, - TransactionStatus.AWAITING_CONFIRMATIONS, - TransactionStatus.AWAITING_EXECUTION, -] -interface AmountData { - decimals?: number | string - symbol?: string - value: number | string -} - -const getAmountWithSymbol = ( - { decimals = 0, symbol = NOT_AVAILABLE, value }: AmountData, - formatted = false, -): string => { - const nonFormattedValue = new BigNumber(value).times(`1e-${decimals}`).toFixed() - const finalValue = formatted ? formatAmount(nonFormattedValue).toString() : nonFormattedValue - const txAmount = finalValue === 'NaN' ? NOT_AVAILABLE : finalValue - - return `${parseFloat(txAmount)} ${symbol}` -} - -export const getTxAmount = (txInfo?: TransactionInfo, formatted = true): string => { - if (!txInfo || !isTransferTxInfo(txInfo)) { - return NOT_AVAILABLE - } - - switch (txInfo.transferInfo.type) { - case TokenType.ERC20: - return getAmountWithSymbol( - { - decimals: `${txInfo.transferInfo.decimals ?? 0}`, - symbol: `${txInfo.transferInfo.tokenSymbol ?? NOT_AVAILABLE}`, - value: txInfo.transferInfo.value, - }, - formatted, - ) - case TokenType.ERC721: - // simple workaround to avoid displaying unexpected values for incoming NFT transfer - return `1 ${txInfo.transferInfo.tokenSymbol}` - case TokenType.NATIVE_COIN: { - const nativeCurrency = getNativeCurrency() - return getAmountWithSymbol( - { - decimals: nativeCurrency.decimals, - symbol: nativeCurrency.symbol, - value: txInfo.transferInfo.value, - }, - formatted, - ) - } - default: - return NOT_AVAILABLE - } -} - -type txTokenData = { - address: string - value: string - decimals: number | null -} - -export const getTxTokenData = (txInfo: Transfer): txTokenData => { - const nativeCurrency = getNativeCurrency() - switch (txInfo.transferInfo.type) { - case TokenType.ERC20: - return { - address: txInfo.transferInfo.tokenAddress, - value: txInfo.transferInfo.value, - decimals: txInfo.transferInfo.decimals, - } - case TokenType.ERC721: - return { address: txInfo.transferInfo.tokenAddress, value: '1', decimals: 0 } - default: - return { - address: getNativeCurrencyAddress(), - value: txInfo.transferInfo.value, - decimals: nativeCurrency.decimals, - } - } -} - -export const isCancelTxDetails = (txInfo: Transaction['txInfo']): boolean => - // custom transaction - isCustomTxInfo(txInfo) && - // flag-based identification - txInfo.isCancellation - export const addressInList = (list: AddressEx[] = []) => (address: string): boolean => list.some((ownerAddress) => sameAddress(ownerAddress.value, address)) -export const getTxTo = ({ txInfo }: Pick): AddressEx | undefined => { - switch (txInfo.type) { - case 'Transfer': { - return txInfo.recipient - } - case 'SettingsChange': { - return undefined - } - case 'Custom': { - return txInfo.to - } - case 'Creation': { - return txInfo.factory || undefined - } - } -} - export const isDeeplinkedTx = (): boolean => { const txMatch = matchPath(history.location.pathname, { path: [SAFE_ROUTES.TRANSACTIONS_HISTORY, SAFE_ROUTES.TRANSACTIONS_QUEUE], @@ -156,127 +29,6 @@ export const isDeeplinkedTx = (): boolean => { return !txMatch && !!deeplinkMatch?.params?.[TRANSACTION_ID_SLUG] } -export const isAwaitingExecution = ( - txStatus: typeof LocalTransactionStatus[keyof typeof LocalTransactionStatus], -): boolean => [LocalTransactionStatus.AWAITING_EXECUTION, LocalTransactionStatus.PENDING_FAILED].includes(txStatus) - -export const makeTransactionDetail = (txDetail: any): any => { - const confirmationList: Array = [] - const RejectedList: Array = [] - - if (txDetail?.Confirmations && txDetail?.Confirmations.length > 0) { - txDetail?.Confirmations.forEach((confirmationItem) => { - const item = { - signature: confirmationItem?.signature, - signer: { - value: confirmationItem?.ownerAddress, - }, - submittedAt: new Date(confirmationItem?.updatedAt).getTime(), - } - confirmationList.push(item) - }) - } - - if (txDetail?.Rejectors && txDetail?.Rejectors?.length > 0) { - txDetail?.Rejectors?.forEach((rejector) => { - const item = { logoUri: null, name: null, value: rejector.ownerAddress } - - RejectedList.push(item) - }) - } - - const signerList: Array = [] - if (txDetail?.Signers && txDetail?.Signers.length > 0) { - txDetail?.Signers.forEach((signerItem) => { - const item = { - value: signerItem?.OwnerAddress, - } - signerList.push(item) - }) - } - return { - executionInfo: { - confirmationsRequired: txDetail?.ConfirmationsRequired, - confirmationsSubmitted: 1, - missingSigners: null, - nonce: txDetail?.Id, - type: 'MULTISIG', - }, - id: txDetail?.Id?.toString(), - safeAppInfo: undefined, - timestamp: new Date(txDetail?.CreatedAt).getTime(), - txDetails: { - detailedExecutionInfo: { - baseGas: txDetail?.GasWanted, - confirmations: confirmationList, - rejectors: RejectedList, - confirmationsRequired: txDetail?.ConfirmationsRequired, - executor: null, - gasPrice: txDetail?.GasPrice, - gasToken: '', - nonce: txDetail?.Id, - refundReceiver: { - value: '', - }, - safeTxGas: txDetail?.GasUsed, - safeTxHash: txDetail?.TxHash, - signers: signerList, - submittedAt: new Date(txDetail?.UpdatedAt).getTime(), - type: 'MULTISIG', - }, - executedAt: null, - safeAddress: txDetail?.FromAddres, - txData: { - dataDecoded: null, - hexData: null, - operation: 0, - to: { - value: txDetail?.ToAddress, - }, - }, - txHash: txDetail?.TxHash, - txId: txDetail?.Id, - txInfo: { - direction: txDetail?.Direction, - recipient: { - value: txDetail?.ToAddress, - name: '', - logoUri: '', - }, - sender: { - value: txDetail?.FromAddres, - name: '', - logoUri: '', - }, - transferInfo: { - type: TokenType.NATIVE_COIN, - value: txDetail?.Amount, - }, - type: 'Transfer', - }, - }, - txInfo: { - direction: txDetail?.Direction, - recipient: { - value: txDetail?.ToAddress, - name: '', - logoUri: '', - }, - sender: { - value: txDetail?.FromAddres, - name: '', - logoUri: '', - }, - transferInfo: { - type: TokenType.NATIVE_COIN, - value: txDetail?.Amount, - }, - typeUrl: txDetail?.TypeUrl, - type: 'Transfer', - }, - txStatus: txDetail.Status, - } -} export const makeHistoryTransactionsFromService = ( list: ITransactionListItem[], currentPayload?: ITransactionListQuery, From 00a1104cb72ee40d3ea8fbc24f3e8d23b4f4c939 Mon Sep 17 00:00:00 2001 From: imhson Date: Fri, 5 May 2023 11:15:11 +0700 Subject: [PATCH 06/69] update token assets --- .../Popup/MultiSendPopup/CreateTxPopup.tsx | 2 +- .../Popup/SendingPopup/CreateTxPopup.tsx | 2 +- .../Popup/SendingPopup/CurrentSafe.tsx | 6 +- .../SafeListSidebar/SafeList/SafeListItem.tsx | 8 +- src/logic/safe/store/models/safe.ts | 4 +- src/logic/safe/store/selectors/index.ts | 2 +- .../safe/utils/__tests__/modules.test.ts | 111 -------- .../safe/utils/__tests__/safeVersion.test.ts | 39 --- .../shouldSafeStoreBeUpdated.test.ts | 242 ------------------ .../safe/utils/__tests__/upgradeSafe.test.ts | 57 ----- .../tokens/store/actions/fetchSafeTokens.ts | 145 ++++------- src/logic/tokens/store/model/token.ts | 3 +- src/pages/Assets/Tokens/index.tsx | 6 +- .../Custom Transaction/ReviewPopup.tsx | 2 +- .../ContractInteraction/ReviewPopup.tsx | 2 +- src/pages/Staking/TxActionModal/Delegate.tsx | 2 +- .../Staking/TxActionModal/Redelegate.tsx | 2 +- .../Staking/TxActionModal/Undelegate.tsx | 2 +- .../TxActionModal/ClaimReward/index.tsx | 2 +- .../ContractInteraction/index.tsx | 2 +- .../TxActionModal/CustomTransaction/index.tsx | 2 +- .../TxActionModal/Delegate/index.tsx | 2 +- .../TxActionModal/EditSequence.tsx | 2 +- .../TxActionModal/MultiSend/index.tsx | 2 +- .../TxActionModal/Redelegate/index.tsx | 2 +- .../Transactions/TxActionModal/Send/index.tsx | 2 +- .../TxActionModal/Undelegate/index.tsx | 2 +- .../Transactions/TxActionModal/Vote/index.tsx | 2 +- .../components/Apps/components/AppFrame.tsx | 10 +- .../ConfirmTxModal/ConfirmTxModal.test.tsx | 18 +- .../ConfirmTxModal/ReviewConfirm.tsx | 4 +- .../Apps/components/ConfirmTxModal/index.tsx | 2 +- .../SignMessageModal/ReviewMessage.tsx | 4 +- .../SignMessageModal.test.tsx | 8 +- .../components/SignMessageModal/index.tsx | 2 +- .../Apps/hooks/useIframeMessageHandler.ts | 6 +- .../Balances/SendModal/SafeInfo/index.tsx | 6 +- .../NativeCoinValue/index.tsx | 10 +- .../SendCustomTx/index.tsx | 8 +- src/utils/index.ts | 7 +- src/utils/safeUtils/selector.ts | 35 ++- 41 files changed, 145 insertions(+), 632 deletions(-) delete mode 100644 src/logic/safe/utils/__tests__/modules.test.ts delete mode 100644 src/logic/safe/utils/__tests__/safeVersion.test.ts delete mode 100644 src/logic/safe/utils/__tests__/shouldSafeStoreBeUpdated.test.ts delete mode 100644 src/logic/safe/utils/__tests__/upgradeSafe.test.ts diff --git a/src/components/Popup/MultiSendPopup/CreateTxPopup.tsx b/src/components/Popup/MultiSendPopup/CreateTxPopup.tsx index 8836f536f3..fe882e8135 100644 --- a/src/components/Popup/MultiSendPopup/CreateTxPopup.tsx +++ b/src/components/Popup/MultiSendPopup/CreateTxPopup.tsx @@ -36,7 +36,7 @@ export default function CreateTxPopup({ gasUsed: string }) { const safeAddress = extractSafeAddress() - const { ethBalance: balance, nextQueueSeq, sequence: currentSequence } = useSelector(currentSafeWithNames) + const { nativeBalance: balance, nextQueueSeq, sequence: currentSequence } = useSelector(currentSafeWithNames) const dispatch = useDispatch() const denom = getCoinMinimalDenom() const chainDefaultGasPrice = getChainDefaultGasPrice() diff --git a/src/components/Popup/SendingPopup/CreateTxPopup.tsx b/src/components/Popup/SendingPopup/CreateTxPopup.tsx index a544a86e58..f0c77e97f2 100644 --- a/src/components/Popup/SendingPopup/CreateTxPopup.tsx +++ b/src/components/Popup/SendingPopup/CreateTxPopup.tsx @@ -38,7 +38,7 @@ export default function CreateTxPopup({ gasUsed: string }) { const safeAddress = extractSafeAddress() - const { ethBalance: balance } = useSelector(currentSafeWithNames) + const { nativeBalance: balance } = useSelector(currentSafeWithNames) const dispatch = useDispatch() const denom = getCoinMinimalDenom() const chainDefaultGasPrice = getChainDefaultGasPrice() diff --git a/src/components/Popup/SendingPopup/CurrentSafe.tsx b/src/components/Popup/SendingPopup/CurrentSafe.tsx index dc269e98d4..3bd8f82874 100644 --- a/src/components/Popup/SendingPopup/CurrentSafe.tsx +++ b/src/components/Popup/SendingPopup/CurrentSafe.tsx @@ -22,7 +22,7 @@ const StyledBlock = styled(Block)` ` const CurrentSafe = (): React.ReactElement => { - const { address: safeAddress, ethBalance, name: safeName } = useSelector(currentSafeWithNames) + const { address: safeAddress, nativeBalance, name: safeName } = useSelector(currentSafeWithNames) const nativeCurrency = getNativeCurrency() return ( @@ -34,11 +34,11 @@ const CurrentSafe = (): React.ReactElement => { showAvatar showCopyBtn /> - {ethBalance && ( + {nativeBalance && ( Balance:{' '} - {`${parseFloat(ethBalance).toFixed(6)} ${ + {`${parseFloat(nativeBalance).toFixed(6)} ${ nativeCurrency.symbol }`} diff --git a/src/components/SafeListSidebar/SafeList/SafeListItem.tsx b/src/components/SafeListSidebar/SafeList/SafeListItem.tsx index e267d1d6e6..93529942bb 100644 --- a/src/components/SafeListSidebar/SafeList/SafeListItem.tsx +++ b/src/components/SafeListSidebar/SafeList/SafeListItem.tsx @@ -88,7 +88,7 @@ type Props = { onSafeClick: () => void onNetworkSwitch?: () => void address: string - ethBalance?: string + nativeBalance?: string showAddSafeLink?: boolean pendingStatus?: SafeStatus | undefined networkId: ChainId @@ -100,7 +100,7 @@ const SafeListItem = ({ onSafeClick, onNetworkSwitch, address, - ethBalance, + nativeBalance, showAddSafeLink = false, pendingStatus = undefined, networkId, @@ -258,9 +258,9 @@ const SafeListItem = ({ shortenHash={[SafeStatus.Pending, SafeStatus.Confirmed].includes(pendingStatus!) ? 100 : 4} /> - {ethBalance ? ( + {nativeBalance ? ( - {formatAmount(ethBalance)} {nativeCurrencySymbol} + {formatAmount(nativeBalance)} {nativeCurrencySymbol} ) : showAddSafeLink ? ( diff --git a/src/logic/safe/store/models/safe.ts b/src/logic/safe/store/models/safe.ts index c6f242a411..5d73e01251 100644 --- a/src/logic/safe/store/models/safe.ts +++ b/src/logic/safe/store/models/safe.ts @@ -28,7 +28,7 @@ export type SafeRecordProps = { chainId?: ChainId safeId?: number threshold: number - ethBalance: string + nativeBalance: string totalFiatBalance: string owners: SafeOwner[] modules?: ModulePair[] | null @@ -56,7 +56,7 @@ const makeSafe = Record({ chainId: undefined, safeId: undefined, threshold: 0, - ethBalance: '0', + nativeBalance: '0', totalFiatBalance: '0', owners: [], modules: [], diff --git a/src/logic/safe/store/selectors/index.ts b/src/logic/safe/store/selectors/index.ts index f515a61811..ebe7ffd424 100644 --- a/src/logic/safe/store/selectors/index.ts +++ b/src/logic/safe/store/selectors/index.ts @@ -28,7 +28,7 @@ const safeFieldSelector = (safe: SafeRecord): SafeRecordProps[K] => safe.get(field, baseSafe().get(field)) -export const currentSafeEthBalance = createSelector(currentSafe, safeFieldSelector('ethBalance')) +export const currentSafeNativeBalance = createSelector(currentSafe, safeFieldSelector('nativeBalance')) export const currentSafeBalances = createSelector(currentSafe, safeFieldSelector('balances')) diff --git a/src/logic/safe/utils/__tests__/modules.test.ts b/src/logic/safe/utils/__tests__/modules.test.ts deleted file mode 100644 index ccaf4b256e..0000000000 --- a/src/logic/safe/utils/__tests__/modules.test.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { SENTINEL_ADDRESS } from 'src/logic/contracts/safeContracts' -import { buildModulesLinkedList } from 'src/logic/safe/utils/modules' - -describe('modules -> buildModulesLinkedList', () => { - let moduleManager - - beforeEach(() => { - moduleManager = { - modules: { - [SENTINEL_ADDRESS]: SENTINEL_ADDRESS, - }, - enableModule: function (module: string) { - this.modules[module] = this.modules[SENTINEL_ADDRESS] - this.modules[SENTINEL_ADDRESS] = module - }, - disableModule: function (prevModule: string, module: string) { - this.modules[prevModule] = this.modules[module] - this.modules[module] = '0x0' - }, - getModules: function (): string[] { - const modules: string[] = [] - let module: string = this.modules[SENTINEL_ADDRESS] - - while (module !== SENTINEL_ADDRESS) { - modules.push(module) - module = this.modules[module] - } - - return modules - }, - } - }) - - it(`should build a collection of addresses pair associated to a linked list`, () => { - // Given - const listOfModules = ['0xa', '0xb', '0xc', '0xd', '0xe', '0xf'] - - // When - const modulesPairList = buildModulesLinkedList(listOfModules) - - // Then - expect(modulesPairList).toStrictEqual([ - [SENTINEL_ADDRESS, '0xa'], - ['0xa', '0xb'], - ['0xb', '0xc'], - ['0xc', '0xd'], - ['0xd', '0xe'], - ['0xe', '0xf'], - ]) - }) - - it(`should properly provide a list of modules pair to remove an specified module`, () => { - // Given - moduleManager.enableModule('0xc') - moduleManager.enableModule('0xb') - moduleManager.enableModule('0xa') // returned list is ordered [0xa, 0xb, 0xc] - const modulesPairList = buildModulesLinkedList(moduleManager.getModules()) - - // When - const moduleBPair = modulesPairList?.[1] ?? [] - moduleManager.disableModule(...moduleBPair) - - // Then - expect(moduleManager.modules['0xb']).toBe('0x0') - expect(moduleManager.getModules()).toStrictEqual(['0xa', '0xc']) - expect(buildModulesLinkedList(moduleManager.getModules())).toStrictEqual([ - [SENTINEL_ADDRESS, '0xa'], - ['0xa', '0xc'], - ]) - }) - - it(`should properly provide a list of modules pair to remove the firstly added module`, () => { - // Given - moduleManager.enableModule('0xc') - moduleManager.enableModule('0xb') - moduleManager.enableModule('0xa') // returned list is ordered [0xa, 0xb, 0xc] - const modulesPairList = buildModulesLinkedList(moduleManager.getModules()) - - // When - const moduleBPair = modulesPairList?.[2] ?? [] - moduleManager.disableModule(...moduleBPair) - - // Then - expect(moduleManager.modules['0xc']).toBe('0x0') - expect(moduleManager.getModules()).toStrictEqual(['0xa', '0xb']) - expect(buildModulesLinkedList(moduleManager.getModules())).toStrictEqual([ - [SENTINEL_ADDRESS, '0xa'], - ['0xa', '0xb'], - ]) - }) - - it(`should properly provide a list of modules pair to remove the lastly added module`, () => { - // Given - moduleManager.enableModule('0xc') - moduleManager.enableModule('0xb') - moduleManager.enableModule('0xa') // returned list is ordered [0xa, 0xb, 0xc] - const modulesPairList = buildModulesLinkedList(moduleManager.getModules()) - - // When - const moduleBPair = modulesPairList?.[0] ?? [] - moduleManager.disableModule(...moduleBPair) - - // Then - expect(moduleManager.modules['0xa']).toBe('0x0') - expect(moduleManager.getModules()).toStrictEqual(['0xb', '0xc']) - expect(buildModulesLinkedList(moduleManager.getModules())).toStrictEqual([ - [SENTINEL_ADDRESS, '0xb'], - ['0xb', '0xc'], - ]) - }) -}) diff --git a/src/logic/safe/utils/__tests__/safeVersion.test.ts b/src/logic/safe/utils/__tests__/safeVersion.test.ts deleted file mode 100644 index 10cfad951c..0000000000 --- a/src/logic/safe/utils/__tests__/safeVersion.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { FEATURES } from '@gnosis.pm/safe-react-gateway-sdk' -import { checkIfSafeNeedsUpdate, hasFeature } from 'src/logic/safe/utils/safeVersion' - -describe('Check safe version', () => { - it('Calls checkIfSafeNeedUpdate, should return true if the safe version is bellow the target one', async () => { - const safeVersion = '1.0.0' - const targetVersion = '1.1.1' - const { needUpdate } = await checkIfSafeNeedsUpdate(safeVersion, targetVersion) - expect(needUpdate).toEqual(true) - }) - it('Calls checkIfSafeNeedUpdate, should return false if the safe version is over the target one', async () => { - const safeVersion = '1.3.0' - const targetVersion = '1.1.1' - const { needUpdate } = await checkIfSafeNeedsUpdate(safeVersion, targetVersion) - expect(needUpdate).toEqual(false) - }) - it('Calls checkIfSafeNeedUpdate, should return false if the safe version is equal the target one', async () => { - const safeVersion = '1.1.1' - const targetVersion = '1.1.1' - const { needUpdate } = await checkIfSafeNeedsUpdate(safeVersion, targetVersion) - expect(needUpdate).toEqual(false) - }) - - describe('hasFeature', () => { - it('returns false for old Safes and SAFE_TX_GAS_OPTIONAL', () => { - expect(hasFeature(FEATURES.SAFE_TX_GAS_OPTIONAL, '1.1.1')).toBe(false) - }) - - it('returns true for new Safes and SAFE_TX_GAS_OPTIONAL', () => { - expect(hasFeature(FEATURES.SAFE_TX_GAS_OPTIONAL, '1.3.0')).toBe(true) - }) - - it('returns true for any Safes and SAFE_APPS', () => { - expect(hasFeature(FEATURES.SAFE_APPS)).toBe(true) - expect(hasFeature(FEATURES.SAFE_APPS, '1.3.0')).toBe(true) - expect(hasFeature(FEATURES.SAFE_APPS, '1.1.1')).toBe(true) - }) - }) -}) diff --git a/src/logic/safe/utils/__tests__/shouldSafeStoreBeUpdated.test.ts b/src/logic/safe/utils/__tests__/shouldSafeStoreBeUpdated.test.ts deleted file mode 100644 index 1d52796bf3..0000000000 --- a/src/logic/safe/utils/__tests__/shouldSafeStoreBeUpdated.test.ts +++ /dev/null @@ -1,242 +0,0 @@ -import { CHAIN_ID } from 'src/config/chain.d' -import { SafeRecordProps } from 'src/logic/safe/store/models/safe' -import { shouldSafeStoreBeUpdated } from 'src/logic/safe/utils/shouldSafeStoreBeUpdated' - -const getMockedOldSafe = ({ - address, - needsUpdate, - balances, - recurringUser, - owners, - featuresEnabled, - currentVersion, - ethBalance, - threshold, - nonce, - modules, - spendingLimits, - guard, -}: Partial): SafeRecordProps => { - const owner1 = '0x3bE3c2dE077FBC409ae50AFFA66a94a9aE669A8d' - const owner2 = '0xA2366b0c2607de70777d87aCdD1D22F0708fA6a3' - const mockedActiveTokenAddress1 = '0x36591cd3DA96b21Ac9ca54cFaf80fe45107294F1' - const mockedActiveTokenAddress2 = '0x92aF97cbF10742dD2527ffaBA70e34C03CFFC2c1' - const mockedGuardAddress = '0x4f8a82d73729A33E0165aDeF3450A7F85f007528' - - return { - address: address || '0xAE173F30ec9A293d37c44BA68d3fCD35F989Ce9F', - chainId: CHAIN_ID.RINKEBY, - threshold: threshold || 2, - ethBalance: ethBalance || '10', - owners: owners || [owner1, owner2], - modules: modules || [], - spendingLimits: spendingLimits || [], - balances: balances || [ - { tokenAddress: mockedActiveTokenAddress1, tokenBalance: '100' }, - { tokenAddress: mockedActiveTokenAddress2, tokenBalance: '10' }, - ], - nonce: nonce || 2, - recurringUser: recurringUser || false, - currentVersion: currentVersion || 'v1.1.1', - needsUpdate: needsUpdate || false, - featuresEnabled: featuresEnabled || [], - totalFiatBalance: '110', - loadedViaUrl: false, - guard: guard || mockedGuardAddress, - collectiblesTag: '0', - txQueuedTag: '0', - txHistoryTag: '0', - } -} - -describe('shouldSafeStoreBeUpdated', () => { - it(`Given two equal safes, should return false`, () => { - // given - const oldSafe = getMockedOldSafe({}) - - // When - const expectedResult = shouldSafeStoreBeUpdated(oldSafe, oldSafe) - - // Then - expect(expectedResult).toEqual(false) - }) - it(`Given an old safe and a new address for the safe, should return true`, () => { - // given - const oldAddress = '0x123' - const newAddress = '0x' - const oldSafe = getMockedOldSafe({ address: oldAddress }) - const newSafeProps: Partial = { - address: newAddress, - } - - // When - const expectedResult = shouldSafeStoreBeUpdated(newSafeProps, oldSafe) - - // Then - expect(expectedResult).toEqual(true) - }) - it(`Given an old safe and a new threshold for the safe, should return true`, () => { - // given - const oldThreshold = 1 - const newThreshold = 2 - const oldSafe = getMockedOldSafe({ threshold: oldThreshold }) - const newSafeProps: Partial = { - threshold: newThreshold, - } - - // When - const expectedResult = shouldSafeStoreBeUpdated(newSafeProps, oldSafe) - - // Then - expect(expectedResult).toEqual(true) - }) - it(`Given an old ethBalance and a new ethBalance for the safe, should return true`, () => { - // given - const oldEthBalance = '1' - const newEthBalance = '2' - const oldSafe = getMockedOldSafe({ ethBalance: oldEthBalance }) - const newSafeProps: Partial = { - ethBalance: newEthBalance, - } - - // When - const expectedResult = shouldSafeStoreBeUpdated(newSafeProps, oldSafe) - - // Then - expect(expectedResult).toEqual(true) - }) - it(`Given an old owners list and a new owners list for the safe, should return true`, () => { - // given - const owner1 = '0x3bE3c2dE077FBC409ae50AFFA66a94a9aE669A8d' - const owner2 = '0xA2366b0c2607de70777d87aCdD1D22F0708fA6a3' - const oldSafe = getMockedOldSafe({ owners: [owner1, owner2] }) - const newSafeProps: Partial = { owners: [owner1] } - - // When - const expectedResult = shouldSafeStoreBeUpdated(newSafeProps, oldSafe) - - // Then - expect(expectedResult).toEqual(true) - }) - it(`Given an old modules list and a new modules list for the safe, should return true`, () => { - // given - const oldModulesList = [] - const newModulesList = null - const oldSafe = getMockedOldSafe({ modules: oldModulesList }) - const newSafeProps: Partial = { modules: newModulesList } - - // When - const expectedResult = shouldSafeStoreBeUpdated(newSafeProps, oldSafe) - - // Then - expect(expectedResult).toEqual(true) - }) - it(`Given an old spendingLimits list and a new spendingLimits list for the safe, should return true`, () => { - // given - const oldSpendingLimitsList = [] - const newSpendingLimitsList = null - const oldSafe = getMockedOldSafe({ spendingLimits: oldSpendingLimitsList }) - const newSafeProps: Partial = { modules: newSpendingLimitsList } - - // When - const expectedResult = shouldSafeStoreBeUpdated(newSafeProps, oldSafe) - - // Then - expect(expectedResult).toEqual(true) - }) - it(`Given an old balances list and a new balances list for the safe, should return true`, () => { - // given - const mockedActiveTokenAddress1 = '0x36591cd3DA96b21Ac9ca54cFaf80fe45107294F1' - const mockedActiveTokenAddress2 = '0x92aF97cbF10742dD2527ffaBA70e34C03CFFC2c1' - const oldBalances = [ - { tokenAddress: mockedActiveTokenAddress1, tokenBalance: '100' }, - { tokenAddress: mockedActiveTokenAddress2, tokenBalance: '100' }, - ] - const newBalances = [{ tokenAddress: mockedActiveTokenAddress1, tokenBalance: '100' }] - const oldSafe = getMockedOldSafe({ balances: oldBalances }) - const newSafeProps: Partial = { - balances: newBalances, - } - - // When - const expectedResult = shouldSafeStoreBeUpdated(newSafeProps, oldSafe) - - // Then - expect(expectedResult).toEqual(true) - }) - it(`Given an old nonce and a new nonce for the safe, should return true`, () => { - // given - const oldNonce = 1 - const newNonce = 2 - const oldSafe = getMockedOldSafe({ nonce: oldNonce }) - const newSafeProps: Partial = { - nonce: newNonce, - } - - // When - const expectedResult = shouldSafeStoreBeUpdated(newSafeProps, oldSafe) - - // Then - expect(expectedResult).toEqual(true) - }) - it(`Given an old recurringUser and a new recurringUser for the safe, should return true`, () => { - // given - const oldRecurringUser = true - const newRecurringUser = false - const oldSafe = getMockedOldSafe({ recurringUser: oldRecurringUser }) - const newSafeProps: Partial = { - recurringUser: newRecurringUser, - } - - // When - const expectedResult = shouldSafeStoreBeUpdated(newSafeProps, oldSafe) - - // Then - expect(expectedResult).toEqual(true) - }) - it(`Given an old recurringUser and a new recurringUser for the safe, should return true`, () => { - // given - const oldCurrentVersion = '1.1.1' - const newCurrentVersion = '1.0.0' - const oldSafe = getMockedOldSafe({ currentVersion: oldCurrentVersion }) - const newSafeProps: Partial = { - currentVersion: newCurrentVersion, - } - - // When - const expectedResult = shouldSafeStoreBeUpdated(newSafeProps, oldSafe) - - // Then - expect(expectedResult).toEqual(true) - }) - it(`Given an old needsUpdate and a new needsUpdate for the safe, should return true`, () => { - // given - const oldNeedsUpdate = false - const newNeedsUpdate = true - const oldSafe = getMockedOldSafe({ needsUpdate: oldNeedsUpdate }) - const newSafeProps: Partial = { - needsUpdate: newNeedsUpdate, - } - - // When - const expectedResult = shouldSafeStoreBeUpdated(newSafeProps, oldSafe) - - // Then - expect(expectedResult).toEqual(true) - }) - it(`Given an old featuresEnabled and a new featuresEnabled for the safe, should return true`, () => { - // given - const oldFeaturesEnabled = [] - const newFeaturesEnabled = undefined - const oldSafe = getMockedOldSafe({ featuresEnabled: oldFeaturesEnabled }) - const newSafeProps: Partial = { - featuresEnabled: newFeaturesEnabled, - } - - // When - const expectedResult = shouldSafeStoreBeUpdated(newSafeProps, oldSafe) - - // Then - expect(expectedResult).toEqual(true) - }) -}) diff --git a/src/logic/safe/utils/__tests__/upgradeSafe.test.ts b/src/logic/safe/utils/__tests__/upgradeSafe.test.ts deleted file mode 100644 index be406d63c3..0000000000 --- a/src/logic/safe/utils/__tests__/upgradeSafe.test.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { getSafeSingletonDeployment, getMultiSendCallOnlyDeployment } from '@gnosis.pm/safe-deployments' -import { AbiItem } from 'web3-utils' -import Web3 from 'web3' - -import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses' -import { encodeMultiSendCall } from 'src/logic/safe/transactions/multisend' -import { GnosisSafe } from 'src/types/contracts/gnosis_safe.d' -import { MultiSend } from 'src/types/contracts/multi_send' - -const SAFE_MASTER_COPY_ADDRESS = '0x34CfAC646f301356fAa8B21e94227e3583Fe3F5F' -const DEFAULT_FALLBACK_HANDLER_ADDRESS = '0xd5D82B6aDDc9027B22dCA772Aa68D5d74cdBdF44' - -jest.mock('src/logic/contracts/safeContracts', () => ({ - getMultisendContract: jest.fn(), -})) - -describe('Upgrade a < 1.3.0 Safe', () => { - const safeContracts = require('src/logic/contracts/safeContracts') - - it('Calls encodeMultiSendCall with a list of MultiSendTransactionInstanceType and returns the multiSend data encoded', () => { - const safeAddress = ZERO_ADDRESS - const web3 = new Web3(new Web3.providers.HttpProvider('')) - - // Mock multisend contract instance - const multiSendCallOnlyDeployment = getMultiSendCallOnlyDeployment() - safeContracts.getMultisendContract.mockReturnValue( - new web3.eth.Contract(multiSendCallOnlyDeployment?.abi as AbiItem[]) as unknown as MultiSend, - ) - - // Mock safe contract instance - const safeSingletonDeployment = getSafeSingletonDeployment({ - version: '1.1.1', - }) - const safeMasterContractAddress = SAFE_MASTER_COPY_ADDRESS - const fallbackHandlerAddress = DEFAULT_FALLBACK_HANDLER_ADDRESS - const safeInstance = new web3.eth.Contract(safeSingletonDeployment?.abi as AbiItem[]) as unknown as GnosisSafe - //@ts-expect-error the method was removed in 1.3.0 contracts - const updateSafeTxData = safeInstance.methods.changeMasterCopy(safeMasterContractAddress).encodeABI() - const fallbackHandlerTxData = safeInstance.methods.setFallbackHandler(fallbackHandlerAddress).encodeABI() - const txs = [ - { - to: safeAddress, - value: '0', - data: updateSafeTxData, - }, - { - to: safeAddress, - value: '0', - data: fallbackHandlerTxData, - }, - ] - const expectedEncodedData = - '0x8d80ff0a000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000f2000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000247de7edef00000000000000000000000034cfac646f301356faa8b21e94227e3583fe3f5f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024f08a0323000000000000000000000000d5d82b6addc9027b22dca772aa68d5d74cdbdf440000000000000000000000000000' - const multiSendTxData = encodeMultiSendCall(txs) - expect(multiSendTxData).toEqual(expectedEncodedData) - }) -}) diff --git a/src/logic/tokens/store/actions/fetchSafeTokens.ts b/src/logic/tokens/store/actions/fetchSafeTokens.ts index 09bd9c718d..804b4ad7de 100644 --- a/src/logic/tokens/store/actions/fetchSafeTokens.ts +++ b/src/logic/tokens/store/actions/fetchSafeTokens.ts @@ -2,19 +2,18 @@ import BigNumber from 'bignumber.js' import { Dispatch } from 'redux' import { SafeBalanceResponse } from '@gnosis.pm/safe-react-gateway-sdk' +import { getChains } from 'src/config/cache/chains' import { currentCurrencySelector } from 'src/logic/currencyValues/store/selectors' import { Errors, logError } from 'src/logic/exceptions/CodedException' -import { fetchTokenCurrenciesBalances, TokenBalance } from 'src/logic/safe/api/fetchTokenCurrenciesBalances' +import { TokenBalance, fetchTokenCurrenciesBalances } from 'src/logic/safe/api/fetchTokenCurrenciesBalances' +import { AppReduxState } from 'src/logic/safe/store' import { updateSafe } from 'src/logic/safe/store/actions/updateSafe' import { currentSafe } from 'src/logic/safe/store/selectors' import { addTokens } from 'src/logic/tokens/store/actions/addTokens' -import { makeToken, Token } from 'src/logic/tokens/store/model/token' +import { Token, makeToken } from 'src/logic/tokens/store/model/token' import { humanReadableValue } from 'src/logic/tokens/utils/humanReadableValue' -import { sameAddress, ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses' -import { getMChainsConfig } from 'src/services/index' -import { AppReduxState } from 'src/logic/safe/store' +import { ZERO_ADDRESS, sameAddress } from 'src/logic/wallets/ethAddresses' import { IMSafeInfo } from 'src/types/safe' -import { getChains } from 'src/config/cache/chains' export type BalanceRecord = { tokenAddress?: string @@ -24,7 +23,7 @@ export type BalanceRecord = { interface ExtractedData { balances: Array - ethBalance: string + nativeBalance: string tokens: Array } @@ -41,7 +40,7 @@ const extractDataFromResult = ( // Extract network token balance from backend balances if (sameAddress(address, ZERO_ADDRESS)) { - acc.ethBalance = humanReadableValue(balance, Number(decimals)) + acc.nativeBalance = humanReadableValue(balance, Number(decimals)) } else { acc.tokens.push(makeToken({ ...tokenInfo })) } @@ -71,11 +70,11 @@ export const fetchSafeTokens = return } - const { balances, ethBalance, tokens } = tokenCurrenciesBalances.items.reduce( + const { balances, nativeBalance, tokens } = tokenCurrenciesBalances.items.reduce( extractDataFromResult, { balances: [], - ethBalance: '0', + nativeBalance: '0', tokens: [], }, ) @@ -84,7 +83,7 @@ export const fetchSafeTokens = updateSafe({ address: safeAddress, balances, - ethBalance: '0', + nativeBalance: '0', totalFiatBalance: new BigNumber(tokenCurrenciesBalances.fiatTotal).toFixed(6), }), ) @@ -92,90 +91,54 @@ export const fetchSafeTokens = } export const fetchMSafeTokens = (safeInfo: IMSafeInfo) => - async (dispatch: Dispatch, getState: () => AppReduxState): Promise => { + async (dispatch: Dispatch): Promise => { if (safeInfo) { - const tokenCurrenciesBalances: SafeBalanceResponse = { - fiatTotal: '0', - items: [], - } - if (safeInfo?.balance) { - // const listChain = await getMChainsConfig() const listChain = getChains() - const decimal: any = listChain.find((x: any) => x.internalChainId === safeInfo?.internalChainId) - safeInfo.balance.forEach((balance) => { - tokenCurrenciesBalances.items.push({ - balance: `${+balance?.amount > 0 ? balance?.amount : 0}`, - fiatBalance: '0', - fiatConversion: '0', - tokenInfo: { - address: '0000000000000000000000000000000000000000', - decimals: decimal.nativeCurrency.decimals, - logoUri: '', - name: 'Aura', - symbol: decimal.nativeCurrency.coinDenom, - }, + const chainInfo: any = listChain.find((x: any) => x.internalChainId === safeInfo?.internalChainId) + const nativeTokenData = safeInfo.balance.find((balance) => balance.denom == chainInfo.denom) + const balances: any[] = [] + if (nativeTokenData) { + balances.push({ + tokenBalance: `${humanReadableValue( + +nativeTokenData?.amount > 0 ? nativeTokenData?.amount : 0, + chainInfo.nativeCurrency.decimals, + )}`, + tokenAddress: '0000000000000000000000000000000000000000', + decimals: chainInfo.nativeCurrency.decimals, + logoUri: chainInfo.nativeCurrency.logoUri, + name: chainInfo.nativeCurrency.name, + symbol: chainInfo.nativeCurrency.symbol, + type: 'native', }) - }) + } + safeInfo.balance + .filter((balance) => balance.denom != chainInfo.denom) + .forEach((data) => { + balances.push({ + tokenBalance: `${humanReadableValue( + +data?.amount > 0 ? data?.amount : 0, + chainInfo.nativeCurrency.decimals, + )}`, + tokenAddress: '111111111111111111111111111111111111111', + decimals: chainInfo.nativeCurrency.decimals, + logoUri: chainInfo.nativeCurrency.logoUri, + name: chainInfo.nativeCurrency.name, + symbol: chainInfo.nativeCurrency.symbol, + type: 'ibc', + }) + }) + const nativeBalance = humanReadableValue( + nativeTokenData?.amount ? nativeTokenData?.amount : '0', + chainInfo.nativeCurrency.decimals, + ) + dispatch( + updateSafe({ + address: safeInfo.address, + balances, + nativeBalance, + }), + ) } - - const { balances, ethBalance, tokens } = tokenCurrenciesBalances.items.reduce( - extractDataFromResult, - { - balances: [], - ethBalance: '0', - tokens: [], - }, - ) - - dispatch( - updateSafe({ - address: safeInfo.address, - balances, - ethBalance, - totalFiatBalance: new BigNumber(tokenCurrenciesBalances.fiatTotal).toFixed(6), - }), - ) - dispatch(addTokens(tokens)) } - - /* - const state = getState() - const safe = currentSafe(state) - - if (!safe) { - return - } - const selectedCurrency = currency ?? currentCurrencySelector(state) - - let tokenCurrenciesBalances: SafeBalanceResponse - try { - tokenCurrenciesBalances = await fetchTokenCurrenciesBalances({ - safeAddress, - selectedCurrency, - }) - } catch (e) { - logError(Errors._601, e.message) - return - } - - const { balances, ethBalance, tokens } = tokenCurrenciesBalances.items.reduce( - extractDataFromResult, - { - balances: [], - ethBalance: '0', - tokens: [], - }, - ) - - dispatch( - updateSafe({ - address: safeAddress, - balances, - ethBalance, - totalFiatBalance: new BigNumber(tokenCurrenciesBalances.fiatTotal).toFixed(2), - }), - ) - dispatch(addTokens(tokens)) - */ } diff --git a/src/logic/tokens/store/model/token.ts b/src/logic/tokens/store/model/token.ts index fe4e781bd9..4248abdefc 100644 --- a/src/logic/tokens/store/model/token.ts +++ b/src/logic/tokens/store/model/token.ts @@ -1,4 +1,3 @@ -import { TokenType } from '@gnosis.pm/safe-react-gateway-sdk' import { Record, RecordOf } from 'immutable' import { BalanceRecord } from 'src/logic/tokens/store/actions/fetchSafeTokens' @@ -10,7 +9,7 @@ type TokenProps = { decimals: number | string logoUri: string | null balance: BalanceRecord - type?: TokenType + type?: string } export const makeToken = Record({ diff --git a/src/pages/Assets/Tokens/index.tsx b/src/pages/Assets/Tokens/index.tsx index 781c5df1d0..6801170c07 100644 --- a/src/pages/Assets/Tokens/index.tsx +++ b/src/pages/Assets/Tokens/index.tsx @@ -5,7 +5,7 @@ import { FilledButton, OutlinedNeutralButton } from 'src/components/Button' import DenseTable, { StyledTableCell, StyledTableRow } from 'src/components/Table/DenseTable' import { useSelector } from 'react-redux' import { extendedSafeTokensSelector } from 'src/utils/safeUtils/selector' -import { formatNativeCurrency } from 'src/utils' +import { formatNativeCurrency, formatWithComma } from 'src/utils' import SendingPopup from 'src/components/Popup/SendingPopup' import sendIcon from 'src/assets/icons/ArrowUpRight.svg' const Wrap = styled.div` @@ -87,8 +87,8 @@ function Tokens(props): ReactElement { {token.name || 'Unkonwn token'} - - {formatNativeCurrency(token.balance.tokenBalance)} + {token.type} + {formatWithComma(token.balance.tokenBalance)}
delegation.operatorAddress == validator.safeStaking, diff --git a/src/pages/Staking/TxActionModal/Redelegate.tsx b/src/pages/Staking/TxActionModal/Redelegate.tsx index 77a6debfeb..1b9c643ec8 100644 --- a/src/pages/Staking/TxActionModal/Redelegate.tsx +++ b/src/pages/Staking/TxActionModal/Redelegate.tsx @@ -23,7 +23,7 @@ import { Wrapper } from './style' export default function Redelegate({ validator, amount, onClose, dstValidator, gasUsed }) { const safeAddress = extractSafeAddress() const dispatch = useDispatch() - const { ethBalance: balance } = useSelector(currentSafeWithNames) + const { nativeBalance: balance } = useSelector(currentSafeWithNames) const delegations = useSelector(allDelegation) const userWalletAddress = useSelector(userAccountSelector) const stakedAmount = delegations?.find( diff --git a/src/pages/Staking/TxActionModal/Undelegate.tsx b/src/pages/Staking/TxActionModal/Undelegate.tsx index 120ecf0047..a7756f33c5 100644 --- a/src/pages/Staking/TxActionModal/Undelegate.tsx +++ b/src/pages/Staking/TxActionModal/Undelegate.tsx @@ -28,7 +28,7 @@ import { Wrapper } from './style' export default function Undelegate({ validator, amount, onClose, gasUsed }) { const safeAddress = extractSafeAddress() const dispatch = useDispatch() - const { ethBalance: balance } = useSelector(currentSafeWithNames) + const { nativeBalance: balance } = useSelector(currentSafeWithNames) const delegations = useSelector(allDelegation) const stakedAmount = delegations?.find( (delegation: any) => delegation.operatorAddress == validator.safeStaking, diff --git a/src/pages/Transactions/TxActionModal/ClaimReward/index.tsx b/src/pages/Transactions/TxActionModal/ClaimReward/index.tsx index 0449d44a95..ad0f04cf01 100644 --- a/src/pages/Transactions/TxActionModal/ClaimReward/index.tsx +++ b/src/pages/Transactions/TxActionModal/ClaimReward/index.tsx @@ -35,7 +35,7 @@ export default function Execute({ const userWalletAddress = useSelector(userAccountSelector) const dispatch = useDispatch() const [sequence, setSequence] = useState(data?.txSequence) - const { ethBalance: balance, nextQueueSeq, sequence: currentSequence } = useSelector(currentSafeWithNames) + const { nativeBalance: balance, nextQueueSeq, sequence: currentSequence } = useSelector(currentSafeWithNames) const txHandler = async (type) => { if (type == 'confirm') { dispatch( diff --git a/src/pages/Transactions/TxActionModal/ContractInteraction/index.tsx b/src/pages/Transactions/TxActionModal/ContractInteraction/index.tsx index de787deffc..022e7e6146 100644 --- a/src/pages/Transactions/TxActionModal/ContractInteraction/index.tsx +++ b/src/pages/Transactions/TxActionModal/ContractInteraction/index.tsx @@ -23,7 +23,7 @@ import { getNotice, getTitle } from '..' import EditSequence from '../EditSequence' import { DeleteButton, TxContent } from '../styles' export default function Execute({ open, onClose, data, sendTx, rejectTx, disabled, setDisabled, deleteTx }) { - const { ethBalance: balance, nextQueueSeq, sequence: currentSequence } = useSelector(currentSafeWithNames) + const { nativeBalance: balance, nextQueueSeq, sequence: currentSequence } = useSelector(currentSafeWithNames) const { action } = useContext(TxSignModalContext) const userWalletAddress = useSelector(userAccountSelector) const dispatch = useDispatch() diff --git a/src/pages/Transactions/TxActionModal/CustomTransaction/index.tsx b/src/pages/Transactions/TxActionModal/CustomTransaction/index.tsx index 62ada2946a..d97efd8360 100644 --- a/src/pages/Transactions/TxActionModal/CustomTransaction/index.tsx +++ b/src/pages/Transactions/TxActionModal/CustomTransaction/index.tsx @@ -35,7 +35,7 @@ export default function Execute({ deleteTx, }) { - const { ethBalance: balance, nextQueueSeq, sequence: currentSequence } = useSelector(currentSafeWithNames) + const { nativeBalance: balance, nextQueueSeq, sequence: currentSequence } = useSelector(currentSafeWithNames) const { action } = useContext(TxSignModalContext) const userWalletAddress = useSelector(userAccountSelector) const [sequence, setSequence] = useState(data?.txSequence) diff --git a/src/pages/Transactions/TxActionModal/Delegate/index.tsx b/src/pages/Transactions/TxActionModal/Delegate/index.tsx index f1be1072ca..04ac29c6fe 100644 --- a/src/pages/Transactions/TxActionModal/Delegate/index.tsx +++ b/src/pages/Transactions/TxActionModal/Delegate/index.tsx @@ -32,7 +32,7 @@ export default function Execute({ deleteTx, }) { - const { ethBalance: balance, nextQueueSeq, sequence: currentSequence } = useSelector(currentSafeWithNames) + const { nativeBalance: balance, nextQueueSeq, sequence: currentSequence } = useSelector(currentSafeWithNames) const { action } = useContext(TxSignModalContext) const delegations = useSelector(allDelegation) const stakedAmount = delegations?.find( diff --git a/src/pages/Transactions/TxActionModal/EditSequence.tsx b/src/pages/Transactions/TxActionModal/EditSequence.tsx index 855c35d22b..7515cd6f04 100644 --- a/src/pages/Transactions/TxActionModal/EditSequence.tsx +++ b/src/pages/Transactions/TxActionModal/EditSequence.tsx @@ -15,7 +15,7 @@ const Wrap = styled.div` } ` export default function EditSequence({ defaultSequence, sequence, setSequence }) { - const { ethBalance: balance, nextQueueSeq, sequence: currentSequence } = useSelector(currentSafeWithNames) + const { nativeBalance: balance, nextQueueSeq, sequence: currentSequence } = useSelector(currentSafeWithNames) const { count, isLoading, hasMore, next, transactions } = usePagedQueuedTransactions() return ( diff --git a/src/pages/Transactions/TxActionModal/MultiSend/index.tsx b/src/pages/Transactions/TxActionModal/MultiSend/index.tsx index b10de6597e..a8ad08ee1e 100644 --- a/src/pages/Transactions/TxActionModal/MultiSend/index.tsx +++ b/src/pages/Transactions/TxActionModal/MultiSend/index.tsx @@ -34,7 +34,7 @@ export default function Execute({ deleteTx, }) { const { action } = useContext(TxSignModalContext) - const { ethBalance: balance, nextQueueSeq, sequence: currentSequence } = useSelector(currentSafeWithNames) + const { nativeBalance: balance, nextQueueSeq, sequence: currentSequence } = useSelector(currentSafeWithNames) const userWalletAddress = useSelector(userAccountSelector) const dispatch = useDispatch() const safeAddress = extractSafeAddress() diff --git a/src/pages/Transactions/TxActionModal/Redelegate/index.tsx b/src/pages/Transactions/TxActionModal/Redelegate/index.tsx index 00ba65b352..a8af1ec352 100644 --- a/src/pages/Transactions/TxActionModal/Redelegate/index.tsx +++ b/src/pages/Transactions/TxActionModal/Redelegate/index.tsx @@ -35,7 +35,7 @@ export default function Execute({ }) { const { action } = useContext(TxSignModalContext) const delegations = useSelector(allDelegation) - const { ethBalance: balance, nextQueueSeq, sequence: currentSequence } = useSelector(currentSafeWithNames) + const { nativeBalance: balance, nextQueueSeq, sequence: currentSequence } = useSelector(currentSafeWithNames) const srcValidatorStakedAmount = delegations?.find( (delegation: any) => delegation.operatorAddress == data?.txDetails?.txMessage[0]?.validatorSrcAddress, )?.staked diff --git a/src/pages/Transactions/TxActionModal/Send/index.tsx b/src/pages/Transactions/TxActionModal/Send/index.tsx index 9d9ae7af1b..4a52af5093 100644 --- a/src/pages/Transactions/TxActionModal/Send/index.tsx +++ b/src/pages/Transactions/TxActionModal/Send/index.tsx @@ -33,7 +33,7 @@ export default function Execute({ deleteTx, }) { const { action } = useContext(TxSignModalContext) - const { ethBalance: balance, nextQueueSeq, sequence: currentSequence } = useSelector(currentSafeWithNames) + const { nativeBalance: balance, nextQueueSeq, sequence: currentSequence } = useSelector(currentSafeWithNames) const userWalletAddress = useSelector(userAccountSelector) const dispatch = useDispatch() const [sequence, setSequence] = useState(data?.txSequence) diff --git a/src/pages/Transactions/TxActionModal/Undelegate/index.tsx b/src/pages/Transactions/TxActionModal/Undelegate/index.tsx index c62fdb74ff..f1977975ff 100644 --- a/src/pages/Transactions/TxActionModal/Undelegate/index.tsx +++ b/src/pages/Transactions/TxActionModal/Undelegate/index.tsx @@ -34,7 +34,7 @@ export default function Execute({ deleteTx, }) { const { action } = useContext(TxSignModalContext) - const { ethBalance: balance, nextQueueSeq, sequence: currentSequence } = useSelector(currentSafeWithNames) + const { nativeBalance: balance, nextQueueSeq, sequence: currentSequence } = useSelector(currentSafeWithNames) const delegations = useSelector(allDelegation) const [sequence, setSequence] = useState(data?.txSequence) diff --git a/src/pages/Transactions/TxActionModal/Vote/index.tsx b/src/pages/Transactions/TxActionModal/Vote/index.tsx index 55f4432266..2a3022e18f 100644 --- a/src/pages/Transactions/TxActionModal/Vote/index.tsx +++ b/src/pages/Transactions/TxActionModal/Vote/index.tsx @@ -40,7 +40,7 @@ export default function Execute({ }) { const { action } = useContext(TxSignModalContext) const [sequence, setSequence] = useState(data?.txSequence) - const { ethBalance: balance, nextQueueSeq, sequence: currentSequence } = useSelector(currentSafeWithNames) + const { nativeBalance: balance, nextQueueSeq, sequence: currentSequence } = useSelector(currentSafeWithNames) const userWalletAddress = useSelector(userAccountSelector) const dispatch = useDispatch() diff --git a/src/routes/safe/components/Apps/components/AppFrame.tsx b/src/routes/safe/components/Apps/components/AppFrame.tsx index 554b6e4d16..f64e5c5f7b 100644 --- a/src/routes/safe/components/Apps/components/AppFrame.tsx +++ b/src/routes/safe/components/Apps/components/AppFrame.tsx @@ -86,7 +86,7 @@ const URL_NOT_PROVIDED_ERROR = 'App url No provided or it is invalid.' const APP_LOAD_ERROR = 'There was an error loading the Safe App. There might be a problem with the App provider.' const AppFrame = ({ appUrl }: Props): ReactElement => { - const { address: safeAddress, ethBalance, owners, threshold } = useSelector(currentSafe) + const { address: safeAddress, nativeBalance, owners, threshold } = useSelector(currentSafe) const { nativeCurrency, chainId, chainName, shortName } = getChainInfo() const safeName = useSelector((state) => addressBookEntryName(state, { address: safeAddress })) const { trackEvent } = useAnalytics() @@ -160,10 +160,10 @@ const AppFrame = ({ appUrl }: Props): ReactElement => { data: { safeAddress: safeAddress as string, network: getChainName().toLowerCase() as LowercaseNetworks, - ethBalance: ethBalance as string, + ethBalance: nativeBalance as string, }, }) - }, [ethBalance, safeAddress, appUrl, sendMessageToIframe]) + }, [nativeBalance, safeAddress, appUrl, sendMessageToIframe]) const communicator = useAppCommunicator(iframeRef, safeApp) @@ -327,7 +327,7 @@ const AppFrame = ({ appUrl }: Props): ReactElement => { isOpen={confirmTransactionModal.isOpen} app={safeApp as SafeApp} safeAddress={safeAddress} - ethBalance={ethBalance as string} + nativeBalance={nativeBalance as string} safeName={safeName as string} txs={confirmTransactionModal.txs} onClose={closeConfirmationModal} @@ -341,7 +341,7 @@ const AppFrame = ({ appUrl }: Props): ReactElement => { isOpen={signMessageModalState.isOpen} app={safeApp as SafeApp} safeAddress={safeAddress} - ethBalance={ethBalance as string} + nativeBalance={nativeBalance as string} safeName={safeName as string} onClose={closeSignMessageModal} requestId={signMessageModalState.requestId} diff --git a/src/routes/safe/components/Apps/components/ConfirmTxModal/ConfirmTxModal.test.tsx b/src/routes/safe/components/Apps/components/ConfirmTxModal/ConfirmTxModal.test.tsx index 6a483b574c..a952f3b471 100644 --- a/src/routes/safe/components/Apps/components/ConfirmTxModal/ConfirmTxModal.test.tsx +++ b/src/routes/safe/components/Apps/components/ConfirmTxModal/ConfirmTxModal.test.tsx @@ -48,7 +48,7 @@ describe('ConfirmTxModal Component', () => { isOpen safeAddress="0x1948fC557ed7219D33138bD2cD52Da7F2047B2bb" safeName="test safe" - ethBalance="100000000000000000" + nativeBalance="100000000000000000" txs={txs} onClose={jest.fn()} onUserConfirm={jest.fn()} @@ -81,7 +81,7 @@ describe('ConfirmTxModal Component', () => { isOpen safeAddress="0x1948fC557ed7219D33138bD2cD52Da7F2047B2bb" safeName="test safe" - ethBalance="100000000000000000" + nativeBalance="100000000000000000" txs={txs} onClose={jest.fn()} onUserConfirm={jest.fn()} @@ -109,7 +109,7 @@ describe('ConfirmTxModal Component', () => { isOpen safeAddress="0x1948fC557ed7219D33138bD2cD52Da7F2047B2bb" safeName="test safe" - ethBalance="100000000000000000" + nativeBalance="100000000000000000" // @ts-expect-error txs are malformed for testing purposes txs={txs} onClose={jest.fn()} @@ -140,7 +140,7 @@ describe('ConfirmTxModal Component', () => { isOpen safeAddress="0x1948fC557ed7219D33138bD2cD52Da7F2047B2bb" safeName="test safe" - ethBalance="100000000000000000" + nativeBalance="100000000000000000" // @ts-expect-error txs are malformed for testing purposes txs={txs} onClose={jest.fn()} @@ -171,7 +171,7 @@ describe('ConfirmTxModal Component', () => { isOpen safeAddress="0x1948fC557ed7219D33138bD2cD52Da7F2047B2bb" safeName="test safe" - ethBalance="100000000000000000" + nativeBalance="100000000000000000" // @ts-expect-error txs are malformed for testing purposes txs={txs} onClose={jest.fn()} @@ -197,7 +197,7 @@ describe('ConfirmTxModal Component', () => { isOpen safeAddress="0x1948fC557ed7219D33138bD2cD52Da7F2047B2bb" safeName="test safe" - ethBalance="100000000000000000" + nativeBalance="100000000000000000" txs={txs} onClose={jest.fn()} onUserConfirm={jest.fn()} @@ -228,7 +228,7 @@ describe('ConfirmTxModal Component', () => { isOpen safeAddress="0x1948fC557ed7219D33138bD2cD52Da7F2047B2bb" safeName="test safe" - ethBalance="100000000000000000" + nativeBalance="100000000000000000" // @ts-expect-error txs are malformed for testing purposes txs={txs} onClose={jest.fn()} @@ -256,7 +256,7 @@ describe('ConfirmTxModal Component', () => { isOpen safeAddress="0x1948fC557ed7219D33138bD2cD52Da7F2047B2bb" safeName="test safe" - ethBalance="100000000000000000" + nativeBalance="100000000000000000" // @ts-expect-error txs are malformed for testing purposes txs={txs} onClose={jest.fn()} @@ -289,7 +289,7 @@ describe('ConfirmTxModal Component', () => { isOpen safeAddress="0x1948fC557ed7219D33138bD2cD52Da7F2047B2bb" safeName="test safe" - ethBalance="100000000000000000" + nativeBalance="100000000000000000" // @ts-expect-error txs are malformed for testing purposes txs={txs} onClose={jest.fn()} diff --git a/src/routes/safe/components/Apps/components/ConfirmTxModal/ReviewConfirm.tsx b/src/routes/safe/components/Apps/components/ConfirmTxModal/ReviewConfirm.tsx index 2739457412..ddd2ef2d20 100644 --- a/src/routes/safe/components/Apps/components/ConfirmTxModal/ReviewConfirm.tsx +++ b/src/routes/safe/components/Apps/components/ConfirmTxModal/ReviewConfirm.tsx @@ -70,7 +70,7 @@ export const ReviewConfirm = ({ app, txs, safeAddress, - ethBalance, + nativeBalance, safeName, params, hidden, @@ -208,7 +208,7 @@ export const ReviewConfirm = ({ Balance: - {`${ethBalance} ${nativeCurrency.symbol}`} + {`${nativeBalance} ${nativeCurrency.symbol}`} diff --git a/src/routes/safe/components/Apps/components/ConfirmTxModal/index.tsx b/src/routes/safe/components/Apps/components/ConfirmTxModal/index.tsx index 88b35c8358..c473fbd57e 100644 --- a/src/routes/safe/components/Apps/components/ConfirmTxModal/index.tsx +++ b/src/routes/safe/components/Apps/components/ConfirmTxModal/index.tsx @@ -19,7 +19,7 @@ export type ConfirmTxModalProps = { safeAddress: string safeName: string requestId: RequestId - ethBalance: string + nativeBalance: string onUserConfirm: (safeTxHash: string, requestId: RequestId) => void onTxReject: (requestId: RequestId) => void onClose: () => void diff --git a/src/routes/safe/components/Apps/components/SignMessageModal/ReviewMessage.tsx b/src/routes/safe/components/Apps/components/SignMessageModal/ReviewMessage.tsx index ecef48c451..3ccc27d798 100644 --- a/src/routes/safe/components/Apps/components/SignMessageModal/ReviewMessage.tsx +++ b/src/routes/safe/components/Apps/components/SignMessageModal/ReviewMessage.tsx @@ -70,7 +70,7 @@ type Props = Omit & { export const ReviewMessage = ({ app, safeAddress, - ethBalance, + nativeBalance, safeName, onUserConfirm, onClose, @@ -185,7 +185,7 @@ export const ReviewMessage = ({ Balance: - {`${ethBalance} ${nativeCurrency.symbol}`} + {`${nativeBalance} ${nativeCurrency.symbol}`} diff --git a/src/routes/safe/components/Apps/components/SignMessageModal/SignMessageModal.test.tsx b/src/routes/safe/components/Apps/components/SignMessageModal/SignMessageModal.test.tsx index 64886eda54..a9e8f5b709 100644 --- a/src/routes/safe/components/Apps/components/SignMessageModal/SignMessageModal.test.tsx +++ b/src/routes/safe/components/Apps/components/SignMessageModal/SignMessageModal.test.tsx @@ -32,7 +32,7 @@ describe('SignMessageModal Component', () => { safeAddress="0x1948fC557ed7219D33138bD2cD52Da7F2047B2bb" message={hexMessage} safeName="test safe" - ethBalance="100000000000000000" + nativeBalance="100000000000000000" onClose={jest.fn()} onUserConfirm={jest.fn()} onTxReject={jest.fn()} @@ -55,7 +55,7 @@ describe('SignMessageModal Component', () => { safeAddress="0x1948fC557ed7219D33138bD2cD52Da7F2047B2bb" message={utf8Message} safeName="test safe" - ethBalance="100000000000000000" + nativeBalance="100000000000000000" onClose={jest.fn()} onUserConfirm={jest.fn()} onTxReject={jest.fn()} @@ -77,7 +77,7 @@ describe('SignMessageModal Component', () => { safeAddress="0x1948fC557ed7219D33138bD2cD52Da7F2047B2bb" message={hexMessage} safeName="test safe" - ethBalance="100000000000000000" + nativeBalance="100000000000000000" onClose={jest.fn()} onUserConfirm={jest.fn()} onTxReject={jest.fn()} @@ -98,7 +98,7 @@ describe('SignMessageModal Component', () => { safeAddress="0x1948fC557ed7219D33138bD2cD52Da7F2047B2bb" message={transactionHash} safeName="test safe" - ethBalance="100000000000000000" + nativeBalance="100000000000000000" onClose={jest.fn()} onUserConfirm={jest.fn()} onTxReject={jest.fn()} diff --git a/src/routes/safe/components/Apps/components/SignMessageModal/index.tsx b/src/routes/safe/components/Apps/components/SignMessageModal/index.tsx index 4f9691fe3f..0a964105c7 100644 --- a/src/routes/safe/components/Apps/components/SignMessageModal/index.tsx +++ b/src/routes/safe/components/Apps/components/SignMessageModal/index.tsx @@ -18,7 +18,7 @@ export type SignMessageModalProps = { safeAddress: string safeName: string requestId: RequestId - ethBalance: string + nativeBalance: string onUserConfirm: (safeTxHash: string, requestId: RequestId) => void onTxReject: (requestId: RequestId) => void onClose: () => void diff --git a/src/routes/safe/components/Apps/hooks/useIframeMessageHandler.ts b/src/routes/safe/components/Apps/hooks/useIframeMessageHandler.ts index 751b38bba0..65e837d572 100644 --- a/src/routes/safe/components/Apps/hooks/useIframeMessageHandler.ts +++ b/src/routes/safe/components/Apps/hooks/useIframeMessageHandler.ts @@ -34,7 +34,7 @@ const useIframeMessageHandler = ( iframeRef: MutableRefObject, ): ReturnType => { const { enqueueSnackbar, closeSnackbar } = useSnackbar() - const { address: safeAddress, ethBalance, name: safeName } = useSelector(currentSafeWithNames) + const { address: safeAddress, nativeBalance, name: safeName } = useSelector(currentSafeWithNames) const dispatch = useDispatch() const sendMessageToIframe = useCallback( @@ -83,7 +83,7 @@ const useIframeMessageHandler = ( data: { safeAddress: safeAddress as string, network: getChainName().toLowerCase() as LowercaseNetworks, - ethBalance: ethBalance as string, + ethBalance: nativeBalance as string, }, } const envInfoMessage = { @@ -129,7 +129,7 @@ const useIframeMessageHandler = ( closeSnackbar, dispatch, enqueueSnackbar, - ethBalance, + nativeBalance, openConfirmationModal, safeAddress, safeName, diff --git a/src/routes/safe/components/Balances/SendModal/SafeInfo/index.tsx b/src/routes/safe/components/Balances/SendModal/SafeInfo/index.tsx index a755a9491e..5d771dff50 100644 --- a/src/routes/safe/components/Balances/SendModal/SafeInfo/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/SafeInfo/index.tsx @@ -22,7 +22,7 @@ const StyledBlock = styled(Block)` ` const SafeInfo = (): React.ReactElement => { - const { address: safeAddress, ethBalance, name: safeName } = useSelector(currentSafeWithNames) + const { address: safeAddress, nativeBalance, name: safeName } = useSelector(currentSafeWithNames) const nativeCurrency = getNativeCurrency() return ( @@ -34,11 +34,11 @@ const SafeInfo = (): React.ReactElement => { showAvatar showCopyBtn /> - {ethBalance && ( + {nativeBalance && ( Balance:{' '} - {`${parseFloat(ethBalance).toFixed(6)} ${ + {`${parseFloat(nativeBalance).toFixed(6)} ${ nativeCurrency.symbol }`} diff --git a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/NativeCoinValue/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/NativeCoinValue/index.tsx index eab33a09e2..2f7b505985 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/NativeCoinValue/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/NativeCoinValue/index.tsx @@ -13,7 +13,7 @@ import Paragraph from 'src/components/layout/Paragraph' import Row from 'src/components/layout/Row' import { isPayable } from 'src/logic/contractInteraction/sources/ABIService' import { styles } from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/style' -import { currentSafeEthBalance } from 'src/logic/safe/store/selectors' +import { currentSafeNativeBalance } from 'src/logic/safe/store/selectors' import { getNativeCurrency } from 'src/config' const useStyles = makeStyles(styles) @@ -25,14 +25,14 @@ interface NativeCoinValueProps { export const NativeCoinValue = ({ onSetMax }: NativeCoinValueProps): React.ReactElement | null => { const classes = useStyles() const nativeCurrency = getNativeCurrency() - const ethBalance = useSelector(currentSafeEthBalance) + const nativeBalance = useSelector(currentSafeNativeBalance) const { input: { value: method }, } = useField('selectedMethod', { subscription: { value: true } }) const disabled = !isPayable(method) - if (!ethBalance) { + if (!nativeBalance) { return null } @@ -44,7 +44,7 @@ export const NativeCoinValue = ({ onSetMax }: NativeCoinValueProps): React.React !disabled && onSetMax(ethBalance)} + onClick={() => !disabled && onSetMax(nativeBalance)} weight="bold" > Send max @@ -63,7 +63,7 @@ export const NativeCoinValue = ({ onSetMax }: NativeCoinValueProps): React.React placeholder="Value" text="Value" type="text" - validate={!disabled && composeValidators(mustBeFloat, maxValue(ethBalance))} + validate={!disabled && composeValidators(mustBeFloat, maxValue(nativeBalance))} /> diff --git a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/SendCustomTx/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/SendCustomTx/index.tsx index 49c7d13d93..37910b4240 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/SendCustomTx/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/SendCustomTx/index.tsx @@ -18,7 +18,7 @@ import Col from 'src/components/layout/Col' import Hairline from 'src/components/layout/Hairline' import Paragraph from 'src/components/layout/Paragraph' import Row from 'src/components/layout/Row' -import { currentSafeEthBalance } from 'src/logic/safe/store/selectors' +import { currentSafeNativeBalance } from 'src/logic/safe/store/selectors' import SafeInfo from 'src/routes/safe/components/Balances/SendModal/SafeInfo' import { styles } from './style' @@ -50,7 +50,7 @@ const useStyles = makeStyles(styles) const SendCustomTx = ({ initialValues, isABI, onClose, onNext, switchMethod }: Props): ReactElement => { const classes = useStyles() const nativeCurrency = getNativeCurrency() - const ethBalance = useSelector(currentSafeEthBalance) + const nativeBalance = useSelector(currentSafeNativeBalance) const saveForm = async (values) => { await handleSubmit(values, false) @@ -83,7 +83,7 @@ const SendCustomTx = ({ initialValues, isABI, onClose, onNext, switchMethod }: P subscription={{ submitting: true, pristine: true, values: true }} > {(submitting, validating, rest, mutators) => { - const handleClickSendMax = () => mutators.setMax(ethBalance) + const handleClickSendMax = () => mutators.setMax(nativeBalance) const handleToggleAbi = () => saveForm(rest.values) return ( <> @@ -116,7 +116,7 @@ const SendCustomTx = ({ initialValues, isABI, onClose, onNext, switchMethod }: P placeholder="Value*" text="Value*" type="text" - validate={composeValidators(mustBeFloat, maxValue(ethBalance || '0'), minValue(0))} + validate={composeValidators(mustBeFloat, maxValue(nativeBalance || '0'), minValue(0))} /> diff --git a/src/utils/index.ts b/src/utils/index.ts index c00334b2f1..96709c1ddd 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -82,7 +82,12 @@ export const formatNativeCurrency = (amount) => { Number(new BigNumber(new BigNumber(+amount).toFixed(+nativeCurrency.decimals)).toFixed()), )} ${nativeCurrency.symbol}` } -const formatWithComma = (amount): string => { + +export const humanReadableValue = (value: number | string, decimals = 18): string => { + return new BigNumber(value).times(`1e-${decimals}`).toFixed() +} + +export const formatWithComma = (amount): string => { if (+amount > 1) { const intl = new Intl.NumberFormat('en-US') return intl.format(amount) diff --git a/src/utils/safeUtils/selector.ts b/src/utils/safeUtils/selector.ts index 133590d561..1762ea3597 100644 --- a/src/utils/safeUtils/selector.ts +++ b/src/utils/safeUtils/selector.ts @@ -5,11 +5,11 @@ import { createSelector } from 'reselect' import { Token } from 'src/logic/tokens/store/model/token' import { tokensSelector } from 'src/logic/tokens/store/selectors' import { getEthAsToken } from 'src/logic/tokens/utils/tokenHelpers' -import { isUserAnOwner, sameAddress } from 'src/logic/wallets/ethAddresses' +import { isUserAnOwner } from 'src/logic/wallets/ethAddresses' import { shouldSwitchWalletChain, userAccountSelector } from 'src/logic/wallets/store/selectors' -import { currentSafe, currentSafeBalances } from 'src/logic/safe/store/selectors' import { SafeRecord } from 'src/logic/safe/store/models/safe' +import { currentSafe, currentSafeBalances } from 'src/logic/safe/store/selectors' export const grantedSelector = createSelector( userAccountSelector, @@ -25,7 +25,7 @@ const safeEthAsTokenSelector = createSelector(currentSafe, (safe?: SafeRecord): return undefined } - return getEthAsToken(safe.ethBalance) + return getEthAsToken(safe.nativeBalance) }) export const extendedSafeTokensSelector = createSelector( @@ -35,28 +35,23 @@ export const extendedSafeTokensSelector = createSelector( (safeBalances, tokensList, ethAsToken): List => { const extendedTokens: Array = [] if (!Array.isArray(safeBalances)) { - // We migrated from immutable Map to array in v3.5.0. Previously stored safes could be still using an object - // to store balances. We add this check to avoid the app to break and refetch the information correctly Sentry.captureMessage( 'There was an error loading `safeBalances` in `extendedSafeTokensSelector`, probably safe loaded prior to v3.5.0', ) return List([]) } - safeBalances.forEach((safeBalance) => { - const tokenAddress = safeBalance.tokenAddress - - if (!tokenAddress) { - return - } - - const baseToken = sameAddress(tokenAddress, ethAsToken?.address) ? ethAsToken : tokensList.get(tokenAddress) - - if (!baseToken) { - return - } - - const token = baseToken.set('balance', safeBalance) - extendedTokens.push(token) + safeBalances.forEach((safeBalance: any) => { + extendedTokens.push({ + address: safeBalance.tokenAddress || '000', + balance: { + tokenBalance: safeBalance.tokenBalance, + }, + decimals: safeBalance.decimals, + name: safeBalance.name, + symbol: safeBalance.symbol, + logoUri: safeBalance.logoUri, + type: safeBalance.type, + } as Token) }) return List(extendedTokens) From b371044d1e6a46b5b3b4393b4f3b58d7bdfcabc3 Mon Sep 17 00:00:00 2001 From: imhson Date: Fri, 5 May 2023 11:23:54 +0700 Subject: [PATCH 07/69] update token assets --- .../tokens/store/actions/fetchSafeTokens.ts | 17 +++++++++++++++++ src/types/safe.d.ts | 4 ++++ 2 files changed, 21 insertions(+) diff --git a/src/logic/tokens/store/actions/fetchSafeTokens.ts b/src/logic/tokens/store/actions/fetchSafeTokens.ts index 804b4ad7de..b5fcb5d1d9 100644 --- a/src/logic/tokens/store/actions/fetchSafeTokens.ts +++ b/src/logic/tokens/store/actions/fetchSafeTokens.ts @@ -128,6 +128,23 @@ export const fetchMSafeTokens = type: 'ibc', }) }) + + if (safeInfo.assets.CW20.asset.length > 0) { + safeInfo.assets.CW20.asset.forEach((data) => { + balances.push({ + tokenBalance: `${humanReadableValue( + +data?.balance > 0 ? data?.balance : 0, + data.asset_info.data.decimals, + )}`, + tokenAddress: data.contract_address, + decimals: data.asset_info.data.decimals, + logoUri: chainInfo.nativeCurrency.logoUri, + name: data.asset_info.data.name, + symbol: data.asset_info.data.symbol, + type: 'CW20', + }) + }) + } const nativeBalance = humanReadableValue( nativeTokenData?.amount ? nativeTokenData?.amount : '0', chainInfo.nativeCurrency.decimals, diff --git a/src/types/safe.d.ts b/src/types/safe.d.ts index 6155c61ed9..f4e1442af6 100644 --- a/src/types/safe.d.ts +++ b/src/types/safe.d.ts @@ -26,6 +26,10 @@ export interface IMSafeInfo { txHistoryTag: string nextQueueSeq: string sequence: string + assets: { + CW20: any + CW721: any + } } export interface IMSafeResponse { From 281835a1eb492012c5ff96a9b37d9d197195ce73 Mon Sep 17 00:00:00 2001 From: imhson Date: Fri, 12 May 2023 14:00:20 +0700 Subject: [PATCH 08/69] add log --- src/logic/tokens/store/actions/fetchSafeTokens.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/logic/tokens/store/actions/fetchSafeTokens.ts b/src/logic/tokens/store/actions/fetchSafeTokens.ts index b5fcb5d1d9..511a1f06dc 100644 --- a/src/logic/tokens/store/actions/fetchSafeTokens.ts +++ b/src/logic/tokens/store/actions/fetchSafeTokens.ts @@ -14,6 +14,7 @@ import { Token, makeToken } from 'src/logic/tokens/store/model/token' import { humanReadableValue } from 'src/logic/tokens/utils/humanReadableValue' import { ZERO_ADDRESS, sameAddress } from 'src/logic/wallets/ethAddresses' import { IMSafeInfo } from 'src/types/safe' +import axios from 'axios' export type BalanceRecord = { tokenAddress?: string @@ -95,6 +96,8 @@ export const fetchMSafeTokens = if (safeInfo) { if (safeInfo?.balance) { const listChain = getChains() + const tokenDetailsList = await axios.get('https://aura-nw.github.io/token-registry/testnet.json') + console.log(tokenDetailsList) const chainInfo: any = listChain.find((x: any) => x.internalChainId === safeInfo?.internalChainId) const nativeTokenData = safeInfo.balance.find((balance) => balance.denom == chainInfo.denom) const balances: any[] = [] From 9dab668e361696ca9d01a10c8e6deb690811aa0e Mon Sep 17 00:00:00 2001 From: imhson Date: Fri, 12 May 2023 15:22:38 +0700 Subject: [PATCH 09/69] get token detail from github page --- src/layout/Root/index.tsx | 9 +++- src/logic/safe/store/actions/fetchSafe.ts | 41 +++++++++++-------- .../tokens/store/actions/fetchSafeTokens.ts | 30 ++++++++------ src/pages/Assets/Tokens/index.tsx | 1 - src/services/data/environment.ts | 3 ++ src/services/index.ts | 8 ++++ 6 files changed, 58 insertions(+), 34 deletions(-) diff --git a/src/layout/Root/index.tsx b/src/layout/Root/index.tsx index b4f9e4710d..bf290fc768 100644 --- a/src/layout/Root/index.tsx +++ b/src/layout/Root/index.tsx @@ -17,7 +17,7 @@ import { CodedException, Errors, logError } from 'src/logic/exceptions/CodedExce import { TermProvider } from 'src/logic/TermContext/index' import AppRoutes from 'src/routes' import { history, WELCOME_ROUTE } from 'src/routes/routes' -import { setBaseUrl } from 'src/services' +import { setBaseUrl, setGithubPageTokenRegistryUrl } from 'src/services' import { getGatewayUrl } from 'src/services/data/environment' import { store } from 'src/logic/safe/store' import theme from 'src/theme/mui' @@ -49,7 +49,7 @@ const RootConsumer = (): React.ReactElement | null => { setIsError(true) return } - const { chainId, apiGateway } = gateway + const { chainId, apiGateway, env } = gateway const localItem = local.getItem(LOCAL_CONFIG_KEY) @@ -59,6 +59,11 @@ const RootConsumer = (): React.ReactElement | null => { if (apiGateway) { setBaseUrl(apiGateway) + setGithubPageTokenRegistryUrl( + env == 'production' + ? 'https://aura-nw.github.io/token-registry/mainnet.json' + : 'https://aura-nw.github.io/token-registry/testnet.json', + ) setGatewayUrl(apiGateway) } else { setIsError(true) diff --git a/src/logic/safe/store/actions/fetchSafe.ts b/src/logic/safe/store/actions/fetchSafe.ts index 78653b05ab..41108b658e 100644 --- a/src/logic/safe/store/actions/fetchSafe.ts +++ b/src/logic/safe/store/actions/fetchSafe.ts @@ -16,11 +16,12 @@ import { fetchCollectibles } from 'src/logic/collectibles/store/actions/fetchCol import { currentChainId } from 'src/logic/config/store/selectors' import { getAccountOnChain, getMSafeInfo } from 'src/services' import { IMSafeInfo } from 'src/types/safe' -import { getCoinDecimal, getInternalChainId, _getChainId } from 'src/config' +import { getCoinDecimal, getInternalChainId, _getChainId, getChainInfo } from 'src/config' import { fetchMSafeTokens } from 'src/logic/tokens/store/actions/fetchSafeTokens' import _ from 'lodash' import BigNumber from 'bignumber.js' import { SequenceResponse } from '@cosmjs/stargate' +import { humanReadableValue } from 'src/utils' /** * Builds a Safe Record that will be added to the app's store @@ -166,34 +167,39 @@ export const fetchMSafe = safeInfo.nextQueueSeq = mSafeInfo?.nextQueueSeq || onlineData?.sequence?.toString() safeInfo.sequence = mSafeInfo?.sequence || onlineData?.sequence?.toString() const coinDecimal = getCoinDecimal() - // If these polling timestamps have changed, fetch again const { txQueuedTag, txHistoryTag, balances } = currentSafeWithNames(state) - const remoteBalances = _.first(mSafeInfo?.balance) - const safeBalance = new BigNumber(remoteBalances?.amount || 0) - .dividedBy(Math.pow(10, coinDecimal)) - .toFixed(coinDecimal) - - let isRefreshTx = false - - if (_.first(balances)?.tokenBalance) { - isRefreshTx = Number(_.first(balances)?.tokenBalance) < Number(safeBalance) + let isBalanceUpdated = false + + mSafeInfo?.balance?.some((balance: any) => { + const decimal = balance?.decimal || coinDecimal + const remoteBalance = humanReadableValue(balance.amount, decimal) + const currentBalance = balances.find((b: any) => b.denom == balance.denom) + if (currentBalance?.tokenBalance != remoteBalance) isBalanceUpdated = true + return isBalanceUpdated + }) + + if (!isBalanceUpdated && mSafeInfo?.assets.CW20.asset.length > 0) { + mSafeInfo?.assets.CW20.asset.some((balance: any) => { + const decimal = balance?.asset_info.data.decimals || coinDecimal + const remoteBalance = humanReadableValue(balance.balance, decimal) + const currentBalance = balances.find((b: any) => b.denom == balance.asset_info.data.symbol) + if (currentBalance?.tokenBalance != remoteBalance) isBalanceUpdated = true + return isBalanceUpdated + }) } + const shouldUpdateTxHistory = txHistoryTag !== safeInfo.txHistoryTag const shouldUpdateTxQueued = txQueuedTag !== safeInfo.txQueuedTag - if (shouldUpdateTxHistory || isRefreshTx || isInitialLoad) { + if (shouldUpdateTxHistory || isInitialLoad) { dispatchPromises.push(dispatch(fetchTransactions(chainId, safeAddress))) } else if (shouldUpdateTxQueued) { dispatchPromises.push(dispatch(fetchTransactions(chainId, safeAddress, true))) } - if (mSafeInfo) { + if ((isBalanceUpdated || isInitialLoad) && mSafeInfo) { dispatchPromises.push(dispatch(fetchMSafeTokens(mSafeInfo))) } - - // if (isInitialLoad) { - // dispatchPromises.push(dispatch(fetchTransactions(chainId, safeAddress))) - // } } const owners = buildSafeOwners(remoteSafeInfo?.owners) @@ -204,7 +210,6 @@ export const fetchMSafe = } async function _getSafeInfo(safeAddress: string, safeId: number): Promise<[IMSafeInfo, SafeInfo]> { - // if (dispatch) await dispatch(fetchMSafeTokens(info)) return getMSafeInfo(safeId).then((mSafeInfo) => { return [ mSafeInfo, diff --git a/src/logic/tokens/store/actions/fetchSafeTokens.ts b/src/logic/tokens/store/actions/fetchSafeTokens.ts index 511a1f06dc..dd361bc640 100644 --- a/src/logic/tokens/store/actions/fetchSafeTokens.ts +++ b/src/logic/tokens/store/actions/fetchSafeTokens.ts @@ -15,6 +15,7 @@ import { humanReadableValue } from 'src/logic/tokens/utils/humanReadableValue' import { ZERO_ADDRESS, sameAddress } from 'src/logic/wallets/ethAddresses' import { IMSafeInfo } from 'src/types/safe' import axios from 'axios' +import { getTokenDetail } from 'src/services' export type BalanceRecord = { tokenAddress?: string @@ -96,8 +97,8 @@ export const fetchMSafeTokens = if (safeInfo) { if (safeInfo?.balance) { const listChain = getChains() - const tokenDetailsList = await axios.get('https://aura-nw.github.io/token-registry/testnet.json') - console.log(tokenDetailsList) + const tokenDetailsListData = await getTokenDetail() + const tokenDetailsList = await tokenDetailsListData.json() const chainInfo: any = listChain.find((x: any) => x.internalChainId === safeInfo?.internalChainId) const nativeTokenData = safeInfo.balance.find((balance) => balance.denom == chainInfo.denom) const balances: any[] = [] @@ -112,28 +113,28 @@ export const fetchMSafeTokens = logoUri: chainInfo.nativeCurrency.logoUri, name: chainInfo.nativeCurrency.name, symbol: chainInfo.nativeCurrency.symbol, + denom: chainInfo.denom, type: 'native', }) } safeInfo.balance .filter((balance) => balance.denom != chainInfo.denom) - .forEach((data) => { + .forEach((data: any) => { balances.push({ - tokenBalance: `${humanReadableValue( - +data?.amount > 0 ? data?.amount : 0, - chainInfo.nativeCurrency.decimals, - )}`, + tokenBalance: `${humanReadableValue(+data?.amount > 0 ? data?.amount : 0, data.decimal)}`, tokenAddress: '111111111111111111111111111111111111111', - decimals: chainInfo.nativeCurrency.decimals, - logoUri: chainInfo.nativeCurrency.logoUri, - name: chainInfo.nativeCurrency.name, - symbol: chainInfo.nativeCurrency.symbol, + decimals: data.decimal, + logoUri: data.logo, + name: data.display, + symbol: data.display, + denom: data.denom, type: 'ibc', }) }) if (safeInfo.assets.CW20.asset.length > 0) { safeInfo.assets.CW20.asset.forEach((data) => { + const tokenDetail = tokenDetailsList.find((token) => token.address == data.contract_address) balances.push({ tokenBalance: `${humanReadableValue( +data?.balance > 0 ? data?.balance : 0, @@ -141,9 +142,12 @@ export const fetchMSafeTokens = )}`, tokenAddress: data.contract_address, decimals: data.asset_info.data.decimals, - logoUri: chainInfo.nativeCurrency.logoUri, - name: data.asset_info.data.name, + name: tokenDetail?.name, + logoUri: tokenDetail?.icon + ? `https://aura-nw.github.io/token-registry/images/${tokenDetail?.icon}` + : 'https://aura-nw.github.io/token-registry/images/undefined.png', symbol: data.asset_info.data.symbol, + denom: data.asset_info.data.symbol, type: 'CW20', }) }) diff --git a/src/pages/Assets/Tokens/index.tsx b/src/pages/Assets/Tokens/index.tsx index 6801170c07..f408068927 100644 --- a/src/pages/Assets/Tokens/index.tsx +++ b/src/pages/Assets/Tokens/index.tsx @@ -78,7 +78,6 @@ function Tokens(props): ReactElement {
{safeTokens.map((token, index) => { - console.log(token) return ( diff --git a/src/services/data/environment.ts b/src/services/data/environment.ts index d06aaa1a56..a39a2ba50b 100644 --- a/src/services/data/environment.ts +++ b/src/services/data/environment.ts @@ -2,6 +2,7 @@ interface IConfiguration { apiGateway: string | null chainId: string | null chainInfo: any[] + env: string } export const getGatewayUrl = async (): Promise => { @@ -12,6 +13,7 @@ export const getGatewayUrl = async (): Promise => { apiGateway: config['api-gateway'], chainInfo: config['chain_info'], chainId: config['chain_id'], + env: config['environment'], } return data }) @@ -21,6 +23,7 @@ export const getGatewayUrl = async (): Promise => { apiGateway: null, chainId: null, chainInfo: [], + env: 'development', } }) } diff --git a/src/services/index.ts b/src/services/index.ts index b64e7a0a82..65138442b6 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -16,6 +16,7 @@ import { import { IMSafeInfo, IMSafeResponse, OwnedMSafes } from '../types/safe' let baseUrl = '' +let githubPageTokenRegistryUrl = '' export interface ISafeCreate { creatorAddress: string @@ -54,6 +55,9 @@ export type MChainInfo = ChainInfo & _ChainInfo export function setBaseUrl(url: string): void { baseUrl = url } +export function setGithubPageTokenRegistryUrl(url: string): void { + githubPageTokenRegistryUrl = url +} export function getMChainsConfig(): Promise { return axios.post(`${baseUrl}/general/network-list`).then((response) => { @@ -278,3 +282,7 @@ export async function getProposalDetail( export async function getContract(contractAddress: string, internalChainId: any): Promise> { return axios.get(`${baseUrl}/contract/${contractAddress}?internalChainId=${internalChainId}`).then((res) => res.data) } + +export async function getTokenDetail() { + return fetch(githubPageTokenRegistryUrl) +} \ No newline at end of file From e7ca204d6e0fd166246b90ead1cc76f98abeab39 Mon Sep 17 00:00:00 2001 From: imhson Date: Mon, 15 May 2023 14:22:33 +0700 Subject: [PATCH 10/69] add env state --- src/layout/Header/components/Layout/Layout.tsx | 13 +++---------- src/layout/Root/index.tsx | 3 ++- src/logic/config/store/actions/index.ts | 2 ++ src/logic/config/store/reducer/index.ts | 4 ++++ src/logic/config/store/reducer/reducer.d.ts | 1 + src/logic/config/store/selectors/index.ts | 3 +++ src/services/index.ts | 14 ++++++-------- 7 files changed, 21 insertions(+), 19 deletions(-) diff --git a/src/layout/Header/components/Layout/Layout.tsx b/src/layout/Header/components/Layout/Layout.tsx index 94e0799aec..5b85c5f331 100644 --- a/src/layout/Header/components/Layout/Layout.tsx +++ b/src/layout/Header/components/Layout/Layout.tsx @@ -16,6 +16,7 @@ import Notifications from '../Notifications' import Provider from '../Provider/Provider' import { DevelopBanner, styles } from './styles' import WalletPopup from './WalletPopup/WalletPopup' +import { currentEnvironment } from 'src/logic/config/store/selectors' const Wrap = styled.div` background: #131419; @@ -42,26 +43,18 @@ const Layout = (props: any) => { const { clickAway, open, toggle } = useStateHandler() const { clickAway: clickAwayNetworks, open: openNetworks, toggle: toggleNetworks } = useStateHandler() const isWrongChain = useSelector(shouldSwitchWalletChain) - const [isProduction, setIsProduction] = useState(false) + const environment = useSelector(currentEnvironment) useEffect(() => { clickAway() }, [showConnect]) - useEffect(() => { - fetch('config.json') - .then((res) => res.json()) - .then((config: any) => { - setIsProduction(config['environment'] == 'production') - }) - }, []) - return ( Aura Safe - {!isProduction && Testnet Only} + {environment == 'development' && Testnet Only} diff --git a/src/layout/Root/index.tsx b/src/layout/Root/index.tsx index bf290fc768..f413921f91 100644 --- a/src/layout/Root/index.tsx +++ b/src/layout/Root/index.tsx @@ -17,7 +17,7 @@ import { CodedException, Errors, logError } from 'src/logic/exceptions/CodedExce import { TermProvider } from 'src/logic/TermContext/index' import AppRoutes from 'src/routes' import { history, WELCOME_ROUTE } from 'src/routes/routes' -import { setBaseUrl, setGithubPageTokenRegistryUrl } from 'src/services' +import { setBaseUrl, setEnv, setGithubPageTokenRegistryUrl } from 'src/services' import { getGatewayUrl } from 'src/services/data/environment' import { store } from 'src/logic/safe/store' import theme from 'src/theme/mui' @@ -59,6 +59,7 @@ const RootConsumer = (): React.ReactElement | null => { if (apiGateway) { setBaseUrl(apiGateway) + setEnv(env || 'development') setGithubPageTokenRegistryUrl( env == 'production' ? 'https://aura-nw.github.io/token-registry/mainnet.json' diff --git a/src/logic/config/store/actions/index.ts b/src/logic/config/store/actions/index.ts index 4ada4ec5e7..a30d2a536e 100644 --- a/src/logic/config/store/actions/index.ts +++ b/src/logic/config/store/actions/index.ts @@ -5,6 +5,8 @@ import { ChainId } from 'src/config/chain.d' export enum CONFIG_ACTIONS { SET_CHAIN_ID = 'config/setChainId', + SET_ENVIRONMENT = 'config/setEnvironment', } export const setChainIdAction = createAction(CONFIG_ACTIONS.SET_CHAIN_ID) +export const setEnvironmentAction = createAction<'production' | 'development'>(CONFIG_ACTIONS.SET_ENVIRONMENT) diff --git a/src/logic/config/store/reducer/index.ts b/src/logic/config/store/reducer/index.ts index 409e6c12fa..ef205aa209 100644 --- a/src/logic/config/store/reducer/index.ts +++ b/src/logic/config/store/reducer/index.ts @@ -7,6 +7,7 @@ export const CONFIG_REDUCER_ID = LOCAL_CONFIG_KEY export const initialConfigState: ConfigState = { chainId: _getChainId(), + environment: 'development', } // Stored locally as to preserve chainId for non-EIP-3770 routes @@ -16,6 +17,9 @@ const configReducer = handleActions( const networkId = action.payload return { ...state, chainId: networkId } }, + [CONFIG_ACTIONS.SET_ENVIRONMENT]: (state, action) => { + return { ...state, environment: action.payload } + }, }, initialConfigState, ) diff --git a/src/logic/config/store/reducer/reducer.d.ts b/src/logic/config/store/reducer/reducer.d.ts index 50aa343d42..e5bc5864f2 100644 --- a/src/logic/config/store/reducer/reducer.d.ts +++ b/src/logic/config/store/reducer/reducer.d.ts @@ -2,6 +2,7 @@ import { ChainId } from 'src/config/chain.d' export type ConfigState = { chainId: ChainId + environment: string } export type ConfigPayload = ChainId diff --git a/src/logic/config/store/selectors/index.ts b/src/logic/config/store/selectors/index.ts index c8b22024e4..10b42d02fd 100644 --- a/src/logic/config/store/selectors/index.ts +++ b/src/logic/config/store/selectors/index.ts @@ -9,4 +9,7 @@ const configState = (state: AppReduxState): ConfigState => state[CONFIG_REDUCER_ export const currentChainId = createSelector([configState], (config): ChainId => { return config.chainId +}) +export const currentEnvironment = createSelector([configState], (config): string => { + return config.environment }) \ No newline at end of file diff --git a/src/services/index.ts b/src/services/index.ts index 65138442b6..b5aebcab42 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -1,22 +1,17 @@ import { SequenceResponse } from '@cosmjs/stargate' -import { ChainInfo, TransferDirection } from '@gnosis.pm/safe-react-gateway-sdk' +import { ChainInfo } from '@gnosis.pm/safe-react-gateway-sdk' import axios from 'axios' import { getChainInfo } from 'src/config' import { WalletKey } from 'src/logic/keplr/keplr' import { CHAIN_THEMES, THEME_DF } from 'src/services/constant/chainThemes' import { getExplorerUrl, getGatewayUrl } from 'src/services/data/environment' import { IProposal, IProposalRes } from 'src/types/proposal' -import { - ICreateSafeTransaction, - ISignSafeTransaction, - ITransactionDetail, - ITransactionListItem, - ITransactionListQuery, -} from 'src/types/transaction' +import { ICreateSafeTransaction, ITransactionListItem, ITransactionListQuery } from 'src/types/transaction' import { IMSafeInfo, IMSafeResponse, OwnedMSafes } from '../types/safe' let baseUrl = '' let githubPageTokenRegistryUrl = '' +let env = 'development' export interface ISafeCreate { creatorAddress: string @@ -58,6 +53,9 @@ export function setBaseUrl(url: string): void { export function setGithubPageTokenRegistryUrl(url: string): void { githubPageTokenRegistryUrl = url } +export function setEnv(e: string): void { + env = e +} export function getMChainsConfig(): Promise { return axios.post(`${baseUrl}/general/network-list`).then((response) => { From 1624b81c7447e2b8a20678e10fc021de6be0b542 Mon Sep 17 00:00:00 2001 From: imhson Date: Wed, 17 May 2023 11:07:20 +0700 Subject: [PATCH 11/69] change token reg --- src/assets/icons/checkIcon.svg | 3 ++ src/components/Input/Checkbox/index.tsx | 31 +++++++++++ src/components/Input/Search/index.tsx | 29 +++++++++++ src/components/Popup/Header.tsx | 4 +- src/components/Popup/index.tsx | 2 +- src/layout/Root/index.tsx | 6 +-- .../tokens/store/actions/fetchSafeTokens.ts | 32 ++++++------ src/logic/tokens/utils/humanReadableValue.ts | 2 +- src/pages/Assets/Tokens/ManageTokenPopup.tsx | 52 +++++++++++++++++++ src/pages/Assets/Tokens/index.tsx | 32 ++++-------- 10 files changed, 147 insertions(+), 46 deletions(-) create mode 100644 src/assets/icons/checkIcon.svg create mode 100644 src/components/Input/Checkbox/index.tsx create mode 100644 src/components/Input/Search/index.tsx create mode 100644 src/pages/Assets/Tokens/ManageTokenPopup.tsx diff --git a/src/assets/icons/checkIcon.svg b/src/assets/icons/checkIcon.svg new file mode 100644 index 0000000000..492f3632ac --- /dev/null +++ b/src/assets/icons/checkIcon.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/Input/Checkbox/index.tsx b/src/components/Input/Checkbox/index.tsx new file mode 100644 index 0000000000..74cab705bd --- /dev/null +++ b/src/components/Input/Checkbox/index.tsx @@ -0,0 +1,31 @@ +import styled from 'styled-components' +import CheckMark from 'src/assets/icons/checkIcon.svg' +const Wrap = styled.input` + width: 20px; + height: 20px; + margin: 0; + appearance: unset; + border-radius: 4px; + transition: all 0.2s ease; + position: relative; + cursor: pointer; + &:not(:checked) { + border: 2px solid #717582; + } + &:checked { + background: #2bbba3; + box-sizing: border-box; + } + &:checked::before { + content: ''; + background: url(${CheckMark}) no-repeat center center; + position: absolute; + width: 20px; + height: 20px; + top: 0; + left: 0; + } +` +export default function Checkbox({ checked, onChange }) { + return onChange(value.target.checked)}> +} diff --git a/src/components/Input/Search/index.tsx b/src/components/Input/Search/index.tsx new file mode 100644 index 0000000000..08517f4145 --- /dev/null +++ b/src/components/Input/Search/index.tsx @@ -0,0 +1,29 @@ +import SearchIcon from 'src/assets/icons/search.svg' +import styled from 'styled-components' + +const Wrap = styled.div` + border: 1px solid #494c58; + border-radius: 8px; + padding: 8px 16px; + gap: 8px; + display: flex; + align-items: center; + input { + font-family: inherit; + font-size: 12px; + line-height: 16px; + background: transparent; + border: none; + outline: none; + color: #fff; + width: 100%; + } +` +export default function SearchInput({ placeholder }) { + return ( + + + + + ) +} diff --git a/src/components/Popup/Header.tsx b/src/components/Popup/Header.tsx index f1f5ee3534..34c907405a 100644 --- a/src/components/Popup/Header.tsx +++ b/src/components/Popup/Header.tsx @@ -44,10 +44,12 @@ export default function Header({ onClose, title, subTitle, + hideNetwork, }: { onClose: () => void title?: string subTitle?: string + hideNetwork?: boolean }) { const connectedNetwork = getChainInfo() return ( @@ -57,7 +59,7 @@ export default function Header({

{subTitle}

- {connectedNetwork.chainId && } + {connectedNetwork.chainId && !hideNetwork && }
diff --git a/src/components/Popup/index.tsx b/src/components/Popup/index.tsx index 507fb6481f..33fbf069d5 100644 --- a/src/components/Popup/index.tsx +++ b/src/components/Popup/index.tsx @@ -33,7 +33,7 @@ interface PopupProps { handleClose?: (event: Record, reason: 'backdropClick' | 'escapeKeyDown') => void open: boolean paperClassName?: string - title: string + title?: string } const Popup = ({ children, description, handleClose, open, paperClassName, title }: PopupProps): ReactElement => { diff --git a/src/layout/Root/index.tsx b/src/layout/Root/index.tsx index f413921f91..7409674a79 100644 --- a/src/layout/Root/index.tsx +++ b/src/layout/Root/index.tsx @@ -60,11 +60,7 @@ const RootConsumer = (): React.ReactElement | null => { if (apiGateway) { setBaseUrl(apiGateway) setEnv(env || 'development') - setGithubPageTokenRegistryUrl( - env == 'production' - ? 'https://aura-nw.github.io/token-registry/mainnet.json' - : 'https://aura-nw.github.io/token-registry/testnet.json', - ) + setGithubPageTokenRegistryUrl(`https://aura-nw.github.io/token-registry/${chainId}.json`) setGatewayUrl(apiGateway) } else { setIsError(true) diff --git a/src/logic/tokens/store/actions/fetchSafeTokens.ts b/src/logic/tokens/store/actions/fetchSafeTokens.ts index dd361bc640..0e53eff0b8 100644 --- a/src/logic/tokens/store/actions/fetchSafeTokens.ts +++ b/src/logic/tokens/store/actions/fetchSafeTokens.ts @@ -120,34 +120,34 @@ export const fetchMSafeTokens = safeInfo.balance .filter((balance) => balance.denom != chainInfo.denom) .forEach((data: any) => { + const tokenDetail = tokenDetailsList['ibc'].find((token) => token.cosmosDenom == data.minimal_denom) balances.push({ - tokenBalance: `${humanReadableValue(+data?.amount > 0 ? data?.amount : 0, data.decimal)}`, - tokenAddress: '111111111111111111111111111111111111111', - decimals: data.decimal, - logoUri: data.logo, - name: data.display, - symbol: data.display, - denom: data.denom, + tokenBalance: `${humanReadableValue(+data?.amount > 0 ? data?.amount : 0, tokenDetail.decimals)}`, + tokenAddress: tokenDetail.address, + decimals: tokenDetail.decimal, + logoUri: tokenDetail?.icon + ? `https://aura-nw.github.io/token-registry/images/${tokenDetail?.icon}` + : 'https://aura-nw.github.io/token-registry/images/undefined.png', + name: tokenDetail.name, + symbol: tokenDetail.coinDenom, + denom: tokenDetail.minCoinDenom, type: 'ibc', }) }) if (safeInfo.assets.CW20.asset.length > 0) { safeInfo.assets.CW20.asset.forEach((data) => { - const tokenDetail = tokenDetailsList.find((token) => token.address == data.contract_address) + const tokenDetail = tokenDetailsList['cw20'].find((token) => token.address == data.contract_address) balances.push({ - tokenBalance: `${humanReadableValue( - +data?.balance > 0 ? data?.balance : 0, - data.asset_info.data.decimals, - )}`, - tokenAddress: data.contract_address, - decimals: data.asset_info.data.decimals, + tokenBalance: `${humanReadableValue(+data?.balance > 0 ? data?.balance : 0, tokenDetail.decimals)}`, + tokenAddress: tokenDetail.address, + decimals: tokenDetail.decimals, name: tokenDetail?.name, logoUri: tokenDetail?.icon ? `https://aura-nw.github.io/token-registry/images/${tokenDetail?.icon}` : 'https://aura-nw.github.io/token-registry/images/undefined.png', - symbol: data.asset_info.data.symbol, - denom: data.asset_info.data.symbol, + symbol: tokenDetail.symbol, + denom: tokenDetail.symbol, type: 'CW20', }) }) diff --git a/src/logic/tokens/utils/humanReadableValue.ts b/src/logic/tokens/utils/humanReadableValue.ts index b3c4ffaca5..7c66e8db52 100644 --- a/src/logic/tokens/utils/humanReadableValue.ts +++ b/src/logic/tokens/utils/humanReadableValue.ts @@ -1,6 +1,6 @@ import { BigNumber } from 'bignumber.js' -export const humanReadableValue = (value: number | string, decimals = 18): string => { +export const humanReadableValue = (value: number | string, decimals = 6): string => { return new BigNumber(value).times(`1e-${decimals}`).toFixed() } diff --git a/src/pages/Assets/Tokens/ManageTokenPopup.tsx b/src/pages/Assets/Tokens/ManageTokenPopup.tsx new file mode 100644 index 0000000000..94d9ae5fc8 --- /dev/null +++ b/src/pages/Assets/Tokens/ManageTokenPopup.tsx @@ -0,0 +1,52 @@ +import { useState } from 'react' +import { FilledButton } from 'src/components/Button' +import Checkbox from 'src/components/Input/Checkbox' +import SearchInput from 'src/components/Input/Search' +import { Popup } from 'src/components/Popup' +import Header from 'src/components/Popup/Header' +import styled from 'styled-components' + +const Wrap = styled.div` + width: 480px; + > div { + padding: 24px; + } + > div:first-child { + margin-bottom: 1px solid #404047; + } + .token-list { + margin-top: 18px; + .title { + font-weight: 600; + font-size: 16px; + line-height: 20px; + } + } +` +const Row = styled.div` + display: flex; + justify-content: space-between; + align-items: center; +` +export default function ManageTokenPopup({ open, onClose }) { + const [toggleAll, setToggleAll] = useState(false) + return ( + +
+ +
+ +
+ +
Token list
+ +
+
+
+
+ Apply +
+
+ + ) +} diff --git a/src/pages/Assets/Tokens/index.tsx b/src/pages/Assets/Tokens/index.tsx index f408068927..91a07f6b31 100644 --- a/src/pages/Assets/Tokens/index.tsx +++ b/src/pages/Assets/Tokens/index.tsx @@ -8,6 +8,8 @@ import { extendedSafeTokensSelector } from 'src/utils/safeUtils/selector' import { formatNativeCurrency, formatWithComma } from 'src/utils' import SendingPopup from 'src/components/Popup/SendingPopup' import sendIcon from 'src/assets/icons/ArrowUpRight.svg' +import ManageTokenPopup from './ManageTokenPopup' +import SearchInput from 'src/components/Input/Search' const Wrap = styled.div` background: ${(props) => props.theme.backgroundPrimary}; border-radius: 8px; @@ -27,23 +29,8 @@ const Wrap = styled.div` font-size: 22px; line-height: 28px; } - .token-search-input { - border: 1px solid #494c58; - border-radius: 8px; - padding: 8px 16px; - gap: 8px; - display: flex; - align-items: center; - input { - font-family: inherit; - font-size: 12px; - line-height: 16px; - background: transparent; - border: none; - outline: none; - color: #fff; - min-width: 300px; - } + .search-input { + min-width: 300px; } } ` @@ -62,6 +49,7 @@ const TokenInfo = styled.div` ` function Tokens(props): ReactElement { const [open, setOpen] = useState(false) + const [manageTokenPopupOpen, setManageTokenPopupOpen] = useState(false) const [selectedToken, setSelectedToken] = useState(undefined) const safeTokens: any = useSelector(extendedSafeTokensSelector) return ( @@ -69,11 +57,10 @@ function Tokens(props): ReactElement {
Token list
-
- - -
- Manage token + + setManageTokenPopupOpen(true)}> + Manage token +
@@ -111,6 +98,7 @@ function Tokens(props): ReactElement { })} {}} onClose={() => setOpen(false)} /> + setManageTokenPopupOpen(false)} /> ) } From 6942211579141c5c84b3e3319ea82575ba3ec277 Mon Sep 17 00:00:00 2001 From: imhson Date: Thu, 18 May 2023 10:16:10 +0700 Subject: [PATCH 12/69] add manage token --- src/config/index.ts | 3 + src/logic/safe/store/actions/fetchSafe.ts | 19 ++- src/logic/safe/store/models/safe.ts | 2 + src/logic/safe/store/reducer/safe.ts | 13 +- .../tokens/store/actions/fetchSafeTokens.ts | 150 +++++++++++------- src/pages/Assets/Tokens/ManageTokenPopup.tsx | 62 +++++++- src/pages/Assets/Tokens/index.tsx | 76 +++++---- src/services/index.ts | 20 ++- src/types/safe.d.ts | 1 + src/utils/index.ts | 4 +- 10 files changed, 237 insertions(+), 113 deletions(-) diff --git a/src/config/index.ts b/src/config/index.ts index 9f243064c2..aed3335958 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -212,3 +212,6 @@ export const getNativeCurrencyLogoUri = (): string => { export const getCoinMinimalDenom = (): string => { return (getChainInfo() as MChainInfo).denom } +export const getCoinConfig = () => { + return (getChainInfo() as any).coinConfig +} diff --git a/src/logic/safe/store/actions/fetchSafe.ts b/src/logic/safe/store/actions/fetchSafe.ts index 41108b658e..a0f4b7c089 100644 --- a/src/logic/safe/store/actions/fetchSafe.ts +++ b/src/logic/safe/store/actions/fetchSafe.ts @@ -170,13 +170,18 @@ export const fetchMSafe = const { txQueuedTag, txHistoryTag, balances } = currentSafeWithNames(state) let isBalanceUpdated = false - mSafeInfo?.balance?.some((balance: any) => { - const decimal = balance?.decimal || coinDecimal - const remoteBalance = humanReadableValue(balance.amount, decimal) - const currentBalance = balances.find((b: any) => b.denom == balance.denom) - if (currentBalance?.tokenBalance != remoteBalance) isBalanceUpdated = true - return isBalanceUpdated - }) + if ((mSafeInfo?.balance?.length || 0) + (mSafeInfo?.assets.CW20.asset?.length || 0) != balances?.length) + isBalanceUpdated = true + + if (!isBalanceUpdated) { + mSafeInfo?.balance?.some((balance: any) => { + const decimal = balance?.decimal || coinDecimal + const remoteBalance = humanReadableValue(balance.amount, decimal) + const currentBalance = balances.find((b: any) => b.denom == balance.denom) + if (currentBalance?.tokenBalance != remoteBalance) isBalanceUpdated = true + return isBalanceUpdated + }) + } if (!isBalanceUpdated && mSafeInfo?.assets.CW20.asset.length > 0) { mSafeInfo?.assets.CW20.asset.some((balance: any) => { diff --git a/src/logic/safe/store/models/safe.ts b/src/logic/safe/store/models/safe.ts index 5d73e01251..fac0f69676 100644 --- a/src/logic/safe/store/models/safe.ts +++ b/src/logic/safe/store/models/safe.ts @@ -46,6 +46,7 @@ export type SafeRecordProps = { txHistoryTag: string nextQueueSeq: string sequence: string + coinConfig?: any[] } /** @@ -74,6 +75,7 @@ const makeSafe = Record({ txHistoryTag: '0', nextQueueSeq: '1', sequence: '1', + coinConfig: [], }) export type SafeRecord = RecordOf diff --git a/src/logic/safe/store/reducer/safe.ts b/src/logic/safe/store/reducer/safe.ts index 2731a142e4..d3f56afb90 100644 --- a/src/logic/safe/store/reducer/safe.ts +++ b/src/logic/safe/store/reducer/safe.ts @@ -33,13 +33,12 @@ const mergeNewTagsInSafe = (state: SafeReducerMap, newSafe: SafeRecord, safeAddr } const updateSafeProps = (prevSafe, safe) => { - return prevSafe.withMutations((record) => { + const nextSafe = prevSafe.withMutations((record) => { // Every property is updated individually to overcome the issue with nested data being overwritten const safeProperties = Object.keys(safe) - // We check each safe property sent in action.payload safeProperties.forEach((key) => { - if (safe[key] && typeof safe[key] === 'object') { + if (safe[key] && typeof safe[key] === 'object' && record.get(key)) { if (safe[key].length >= 0 || Map.isMap(safe[key])) { // If type is array we replace it // If type is Immutable Map we replace it @@ -57,6 +56,7 @@ const updateSafeProps = (prevSafe, safe) => { } }) }) + return nextSafe } type Payloads = SafeRecord | string @@ -70,11 +70,10 @@ const safeReducer = handleActions( mergeNewTagsInSafe(state, safe, safeAddress) const shouldUpdate = shouldSafeStoreBeUpdated(safe, state.getIn(['safes', safeAddress]) as SafeRecordProps) - return shouldUpdate - ? state.updateIn(['safes', safeAddress], makeSafe({ address: safeAddress }), (prevSafe) => - updateSafeProps(prevSafe, safe), - ) + ? state.updateIn(['safes', safeAddress], makeSafe({ address: safeAddress }), (prevSafe) => { + return updateSafeProps(prevSafe, safe) + }) : state }, [ADD_OR_UPDATE_SAFE]: (state, action: Action) => { diff --git a/src/logic/tokens/store/actions/fetchSafeTokens.ts b/src/logic/tokens/store/actions/fetchSafeTokens.ts index 0e53eff0b8..0756e64113 100644 --- a/src/logic/tokens/store/actions/fetchSafeTokens.ts +++ b/src/logic/tokens/store/actions/fetchSafeTokens.ts @@ -16,6 +16,7 @@ import { ZERO_ADDRESS, sameAddress } from 'src/logic/wallets/ethAddresses' import { IMSafeInfo } from 'src/types/safe' import axios from 'axios' import { getTokenDetail } from 'src/services' +import { getCoinConfig } from 'src/config' export type BalanceRecord = { tokenAddress?: string @@ -91,78 +92,105 @@ export const fetchSafeTokens = ) dispatch(addTokens(tokens)) } + export const fetchMSafeTokens = (safeInfo: IMSafeInfo) => - async (dispatch: Dispatch): Promise => { - if (safeInfo) { - if (safeInfo?.balance) { - const listChain = getChains() - const tokenDetailsListData = await getTokenDetail() - const tokenDetailsList = await tokenDetailsListData.json() - const chainInfo: any = listChain.find((x: any) => x.internalChainId === safeInfo?.internalChainId) - const nativeTokenData = safeInfo.balance.find((balance) => balance.denom == chainInfo.denom) - const balances: any[] = [] - if (nativeTokenData) { - balances.push({ - tokenBalance: `${humanReadableValue( - +nativeTokenData?.amount > 0 ? nativeTokenData?.amount : 0, - chainInfo.nativeCurrency.decimals, - )}`, - tokenAddress: '0000000000000000000000000000000000000000', - decimals: chainInfo.nativeCurrency.decimals, - logoUri: chainInfo.nativeCurrency.logoUri, - name: chainInfo.nativeCurrency.name, - symbol: chainInfo.nativeCurrency.symbol, - denom: chainInfo.denom, - type: 'native', - }) - } - safeInfo.balance - .filter((balance) => balance.denom != chainInfo.denom) - .forEach((data: any) => { - const tokenDetail = tokenDetailsList['ibc'].find((token) => token.cosmosDenom == data.minimal_denom) - balances.push({ - tokenBalance: `${humanReadableValue(+data?.amount > 0 ? data?.amount : 0, tokenDetail.decimals)}`, - tokenAddress: tokenDetail.address, - decimals: tokenDetail.decimal, - logoUri: tokenDetail?.icon - ? `https://aura-nw.github.io/token-registry/images/${tokenDetail?.icon}` - : 'https://aura-nw.github.io/token-registry/images/undefined.png', + async (dispatch: Dispatch, getState: () => AppReduxState): Promise => { + const state = getState() + const safe = currentSafe(state) + if (safeInfo?.balance) { + const coinConfig = + safe?.coinConfig || + getCoinConfig().map((config) => { + return { ...config, enable: true } + }) + const listChain = getChains() + const tokenDetailsListData = await getTokenDetail() + const tokenDetailsList = await tokenDetailsListData.json() + const chainInfo: any = listChain.find((x: any) => x.internalChainId === safeInfo?.internalChainId) + const nativeTokenData = safeInfo.balance.find((balance) => balance.denom == chainInfo.denom) + const balances: any[] = [] + if (nativeTokenData) { + balances.push({ + tokenBalance: `${humanReadableValue( + +nativeTokenData?.amount > 0 ? nativeTokenData?.amount : 0, + chainInfo.nativeCurrency.decimals, + )}`, + tokenAddress: '0000000000000000000000000000000000000000', + decimals: chainInfo.nativeCurrency.decimals, + logoUri: chainInfo.nativeCurrency.logoUri, + name: chainInfo.nativeCurrency.name, + symbol: chainInfo.nativeCurrency.symbol, + denom: chainInfo.denom, + type: 'native', + }) + } + safeInfo.balance + .filter((balance) => balance.denom != chainInfo.denom) + .forEach((data: any) => { + const tokenDetail = tokenDetailsList['ibc'].find((token) => token.cosmosDenom == data.minimal_denom) + if (coinConfig.findIndex((config) => config.address == tokenDetail.address) == -1) { + coinConfig.push({ name: tokenDetail.name, - symbol: tokenDetail.coinDenom, + address: tokenDetail.address, denom: tokenDetail.minCoinDenom, type: 'ibc', + enable: false, }) + } + balances.push({ + tokenBalance: `${humanReadableValue(+data?.amount > 0 ? data?.amount : 0, tokenDetail.decimals)}`, + tokenAddress: tokenDetail.address, + decimals: tokenDetail.decimal, + logoUri: tokenDetail?.icon + ? `https://aura-nw.github.io/token-registry/images/${tokenDetail?.icon}` + : 'https://aura-nw.github.io/token-registry/images/undefined.png', + name: tokenDetail.name, + symbol: tokenDetail.coinDenom, + denom: tokenDetail.minCoinDenom, + type: 'ibc', }) + }) - if (safeInfo.assets.CW20.asset.length > 0) { - safeInfo.assets.CW20.asset.forEach((data) => { - const tokenDetail = tokenDetailsList['cw20'].find((token) => token.address == data.contract_address) - balances.push({ - tokenBalance: `${humanReadableValue(+data?.balance > 0 ? data?.balance : 0, tokenDetail.decimals)}`, - tokenAddress: tokenDetail.address, - decimals: tokenDetail.decimals, - name: tokenDetail?.name, - logoUri: tokenDetail?.icon - ? `https://aura-nw.github.io/token-registry/images/${tokenDetail?.icon}` - : 'https://aura-nw.github.io/token-registry/images/undefined.png', - symbol: tokenDetail.symbol, + if (safeInfo.assets.CW20.asset.length > 0) { + safeInfo.assets.CW20.asset.forEach((data) => { + const tokenDetail = tokenDetailsList['cw20'].find((token) => token.address == data.contract_address) + if (coinConfig.findIndex((config) => config.address == tokenDetail.address) == -1) { + coinConfig.push({ + name: tokenDetail.name, + address: tokenDetail.address, denom: tokenDetail.symbol, - type: 'CW20', + type: 'cw20', + enable: false, }) + } + balances.push({ + tokenBalance: `${humanReadableValue(+data?.balance > 0 ? data?.balance : 0, tokenDetail.decimals)}`, + tokenAddress: tokenDetail.address, + decimals: tokenDetail.decimals, + name: tokenDetail?.name, + logoUri: tokenDetail?.icon + ? `https://aura-nw.github.io/token-registry/images/${tokenDetail?.icon}` + : 'https://aura-nw.github.io/token-registry/images/undefined.png', + symbol: tokenDetail.symbol, + denom: tokenDetail.symbol, + type: 'CW20', }) - } - const nativeBalance = humanReadableValue( - nativeTokenData?.amount ? nativeTokenData?.amount : '0', - chainInfo.nativeCurrency.decimals, - ) - dispatch( - updateSafe({ - address: safeInfo.address, - balances, - nativeBalance, - }), - ) + }) } + + const nativeBalance = humanReadableValue( + nativeTokenData?.amount ? nativeTokenData?.amount : '0', + chainInfo.nativeCurrency.decimals, + ) + + dispatch( + updateSafe({ + address: safeInfo.address, + balances, + nativeBalance, + coinConfig, + }), + ) } } diff --git a/src/pages/Assets/Tokens/ManageTokenPopup.tsx b/src/pages/Assets/Tokens/ManageTokenPopup.tsx index 94d9ae5fc8..8d29ca4a9d 100644 --- a/src/pages/Assets/Tokens/ManageTokenPopup.tsx +++ b/src/pages/Assets/Tokens/ManageTokenPopup.tsx @@ -1,9 +1,12 @@ import { useState } from 'react' +import { useDispatch, useSelector } from 'react-redux' import { FilledButton } from 'src/components/Button' import Checkbox from 'src/components/Input/Checkbox' import SearchInput from 'src/components/Input/Search' import { Popup } from 'src/components/Popup' import Header from 'src/components/Popup/Header' +import { updateSafe } from 'src/logic/safe/store/actions/updateSafe' +import { currentSafeWithNames } from 'src/logic/safe/store/selectors' import styled from 'styled-components' const Wrap = styled.div` @@ -12,7 +15,7 @@ const Wrap = styled.div` padding: 24px; } > div:first-child { - margin-bottom: 1px solid #404047; + border-bottom: 1px solid #404047; } .token-list { margin-top: 18px; @@ -21,6 +24,12 @@ const Wrap = styled.div` font-size: 16px; line-height: 20px; } + .list { + margin-top: 16px; + border-radius: 8px; + padding: 0px 16px; + background: #363843; + } } ` const Row = styled.div` @@ -29,7 +38,19 @@ const Row = styled.div` align-items: center; ` export default function ManageTokenPopup({ open, onClose }) { + const dispatch = useDispatch() const [toggleAll, setToggleAll] = useState(false) + const { coinConfig, address } = useSelector(currentSafeWithNames) + const [config, setConfig] = useState(coinConfig) + + const applyHandler = () => { + dispatch( + updateSafe({ + address, + coinConfig: config, + }), + ) + } return (
@@ -39,14 +60,47 @@ export default function ManageTokenPopup({ open, onClose }) {
Token list
- +
+ +
+
+ {config?.map((c, i) => { + return ( + setConfig(config.map((cc, ii) => (i == ii ? { ...cc, enable: !cc.enable } : cc)))} + /> + ) + })} +
-
- Apply +
+ applyHandler()}>Apply
) } + +const CoinWrapper = styled.div` + margin: 8px 0px; + min-height: 40px; + display: flex; + justify-content: space-between; + align-items: center; + > div { + text-transform: uppercase; + } +` +const CoinConfig = ({ name, isEnable, setToggle }) => { + return ( + +
{name}
+ setToggle()} /> +
+ ) +} diff --git a/src/pages/Assets/Tokens/index.tsx b/src/pages/Assets/Tokens/index.tsx index 91a07f6b31..93d67e6abb 100644 --- a/src/pages/Assets/Tokens/index.tsx +++ b/src/pages/Assets/Tokens/index.tsx @@ -10,6 +10,7 @@ import SendingPopup from 'src/components/Popup/SendingPopup' import sendIcon from 'src/assets/icons/ArrowUpRight.svg' import ManageTokenPopup from './ManageTokenPopup' import SearchInput from 'src/components/Input/Search' +import { currentSafeWithNames } from 'src/logic/safe/store/selectors' const Wrap = styled.div` background: ${(props) => props.theme.backgroundPrimary}; border-radius: 8px; @@ -52,6 +53,8 @@ function Tokens(props): ReactElement { const [manageTokenPopupOpen, setManageTokenPopupOpen] = useState(false) const [selectedToken, setSelectedToken] = useState(undefined) const safeTokens: any = useSelector(extendedSafeTokensSelector) + const { coinConfig, address } = useSelector(currentSafeWithNames) + return (
@@ -64,38 +67,47 @@ function Tokens(props): ReactElement {
- {safeTokens.map((token, index) => { - return ( - - - - - {token.name || 'Unkonwn token'} - - - {token.type} - {formatWithComma(token.balance.tokenBalance)} - -
- { - setOpen(true) - setSelectedToken(token.address) - }} - > - - Send - - - - Receive - -
-
-
- ) - })} + {safeTokens + .filter((token) => { + return ( + token.type == 'native' || + coinConfig?.find((coin) => { + return coin.address == token.address + })?.enable + ) + }) + .map((token, index) => { + return ( + + + + + {token.name || 'Unkonwn token'} + + + {token.type} + {formatWithComma(token.balance.tokenBalance)} + +
+ { + setOpen(true) + setSelectedToken(token.address) + }} + > + + Send + + + + Receive + +
+
+
+ ) + })}
{}} onClose={() => setOpen(false)} /> setManageTokenPopupOpen(false)} /> diff --git a/src/services/index.ts b/src/services/index.ts index b5aebcab42..1d677f6eb5 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -74,6 +74,7 @@ export function getMChainsConfig(): Promise { defaultGas: GasPriceDefault[] tokenImg: string rest: string + coinConfig?: any[] }) => { return { transactionService: null, @@ -125,6 +126,23 @@ export function getMChainsConfig(): Promise { // 'SAFE_TX_GAS_OPTIONAL', // 'SPENDING_LIMIT', ], + coinConfig: e?.coinConfig || [ + { + name: 'AURA', + }, + { + name: 'BTC', + }, + { + name: 'ETH', + }, + { + name: 'BNB', + }, + { + name: 'USDT', + }, + ], } }, ) @@ -283,4 +301,4 @@ export async function getContract(contractAddress: string, internalChainId: any) export async function getTokenDetail() { return fetch(githubPageTokenRegistryUrl) -} \ No newline at end of file +} diff --git a/src/types/safe.d.ts b/src/types/safe.d.ts index f4e1442af6..19663dd12a 100644 --- a/src/types/safe.d.ts +++ b/src/types/safe.d.ts @@ -30,6 +30,7 @@ export interface IMSafeInfo { CW20: any CW721: any } + coinConfig: any[] } export interface IMSafeResponse { diff --git a/src/utils/index.ts b/src/utils/index.ts index 96709c1ddd..753ec344ab 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -89,7 +89,9 @@ export const humanReadableValue = (value: number | string, decimals = 18): strin export const formatWithComma = (amount): string => { if (+amount > 1) { - const intl = new Intl.NumberFormat('en-US') + const intl = new Intl.NumberFormat('en-US', { + maximumFractionDigits: 6, + }) return intl.format(amount) } else { return amount?.toString() From d471e69e83aa1febfb479d1093aadde96c2016d8 Mon Sep 17 00:00:00 2001 From: imhson Date: Fri, 19 May 2023 10:52:28 +0700 Subject: [PATCH 13/69] add linter for custom tx --- .../Custom Transaction/MessageGenerator.tsx | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/src/pages/Avanced/Custom Transaction/MessageGenerator.tsx b/src/pages/Avanced/Custom Transaction/MessageGenerator.tsx index 02d3782deb..219c1ecac9 100644 --- a/src/pages/Avanced/Custom Transaction/MessageGenerator.tsx +++ b/src/pages/Avanced/Custom Transaction/MessageGenerator.tsx @@ -1,14 +1,12 @@ -import { Accordion, AccordionDetails, AccordionSummary } from '@aura/safe-react-components' +import { json, jsonParseLinter } from '@codemirror/lang-json' +import { githubDark } from '@uiw/codemirror-theme-github' +import CodeMirror from '@uiw/react-codemirror' import { ReactElement, useEffect, useState } from 'react' -import TextArea from 'src/components/Input/TextArea' +import { Message } from 'src/components/CustomTransactionMessage/BigMsg' import { getInternalChainId } from 'src/config' import styled from 'styled-components' -import CodeMirror from '@uiw/react-codemirror' -import { githubDark } from '@uiw/codemirror-theme-github' -import { StreamLanguage } from '@codemirror/language' -import { json } from '@codemirror/lang-json' -import { javascript } from '@codemirror/lang-javascript' -import { Message } from 'src/components/CustomTransactionMessage/BigMsg' + +import { linter } from '@codemirror/lint' const Wrap = styled.div` display: flex; @@ -55,8 +53,6 @@ function MessageGenerator({ setMessage, setIsError }): ReactElement { } setIsError(false) const parsedMessage = JSON.parse(msg) - const prettyJson = JSON.stringify(parsedMessage, undefined, 4) - setRawMsg(prettyJson) if (typeof parsedMessage !== 'object' || !Array.isArray(parsedMessage)) { throw new Error('Input data is not an array') } @@ -69,6 +65,17 @@ function MessageGenerator({ setMessage, setIsError }): ReactElement { setErrorMsg(error.message) } } + + const beutifyJson = () => { + try { + if (!errorMsg) { + const parsedMessage = JSON.parse(rawMsg) + const prettyJson = JSON.stringify(parsedMessage, undefined, 4) + setRawMsg(prettyJson) + } + } catch (error) {} + } + useEffect(() => { if (errorMsg) { setIsError(true) @@ -82,10 +89,11 @@ function MessageGenerator({ setMessage, setIsError }): ReactElement { onMsgChange(value)} + onBlur={() => beutifyJson()} placeholder={`[ { "typeUrl": "...", From 4224ba032fd57706e40034bc189e25d526e4b128 Mon Sep 17 00:00:00 2001 From: imhson Date: Fri, 19 May 2023 11:07:49 +0700 Subject: [PATCH 14/69] add warning failed estimate gas --- .../Avanced/Custom Transaction/ReviewPopup.tsx | 10 ++++++++-- src/pages/Avanced/Custom Transaction/styles.tsx | 16 +++++++++++++++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/pages/Avanced/Custom Transaction/ReviewPopup.tsx b/src/pages/Avanced/Custom Transaction/ReviewPopup.tsx index 16d1947f9d..7495b3b764 100644 --- a/src/pages/Avanced/Custom Transaction/ReviewPopup.tsx +++ b/src/pages/Avanced/Custom Transaction/ReviewPopup.tsx @@ -19,7 +19,7 @@ import { extractSafeAddress } from 'src/routes/routes' import { formatNativeCurrency, formatNumber } from 'src/utils' import { signAndCreateTransaction } from 'src/utils/signer' import { Wrap } from './styles' - +import AlertIcon from 'src/assets/icons/alert.svg' export default function ReviewPopup({ open, setOpen, gasUsed, msg }) { const safeAddress = extractSafeAddress() const dispatch = useDispatch() @@ -63,7 +63,7 @@ export default function ReviewPopup({ open, setOpen, gasUsed, msg }) { }) } }) - const gasUsed = (200000 * msg.length).toString() + const gasUsed = (250000 * msg.length).toString() setManualGasLimit(gasUsed) const gasFee = calculateGasFee(+gasUsed, +chainDefaultGasPrice, decimal) setGasPriceFormatted(gasFee) @@ -105,6 +105,12 @@ export default function ReviewPopup({ open, setOpen, gasUsed, msg }) { })} +
+
+ +
+
Failed to estimate gas. Default value applied.
+
Date: Fri, 19 May 2023 13:56:36 +0700 Subject: [PATCH 15/69] enable custom tx --- src/layout/Sidebar/useSidebarItems.tsx | 12 ++++++------ src/routes/safe/index.tsx | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/layout/Sidebar/useSidebarItems.tsx b/src/layout/Sidebar/useSidebarItems.tsx index 3920329330..4341638734 100644 --- a/src/layout/Sidebar/useSidebarItems.tsx +++ b/src/layout/Sidebar/useSidebarItems.tsx @@ -111,12 +111,12 @@ const useSidebarItems = (): ListItemType[] => { href: currentSafeRoutes.CONTRACT_INTERACTION, subItems: smartContractSubItems, }), - // makeEntryItem({ - // label: 'Advanced', - // iconType: 'smartContractAura', - // href: currentSafeRoutes.CUSTOM_TRANSACTION, - // subItems: advancedSubItems, - // }), + makeEntryItem({ + label: 'Advanced', + iconType: 'smartContractAura', + href: currentSafeRoutes.CUSTOM_TRANSACTION, + subItems: advancedSubItems, + }), makeEntryItem({ label: 'Address Book', iconType: 'addressbookAura', diff --git a/src/routes/safe/index.tsx b/src/routes/safe/index.tsx index b575dc852c..d00c72c1a6 100644 --- a/src/routes/safe/index.tsx +++ b/src/routes/safe/index.tsx @@ -107,7 +107,7 @@ const Container = (): React.ReactElement => { path={SAFE_ROUTES.CONTRACT_INTERACTION} render={() => wrapInSuspense(, null)} /> - {/* wrapInSuspense(, null)} /> */} + wrapInSuspense(, null)} /> wrapInSuspense(, null)} /> wrapInSuspense(, null)} /> wrapInSuspense(, null)} /> From 77927941a4ef62ad3d251e61126fa2a8bb2ff518 Mon Sep 17 00:00:00 2001 From: imhson Date: Tue, 23 May 2023 14:09:24 +0700 Subject: [PATCH 16/69] update manage token & fix first safe load --- src/App/index.tsx | 14 +- src/components/Input/AmountInput/index.tsx | 6 +- src/components/Input/Token/index.tsx | 11 +- .../Popup/SendingPopup/CreateTxPopup.tsx | 18 +- src/components/Popup/SendingPopup/index.tsx | 36 ++-- src/components/TxComponents/Amount.tsx | 13 +- src/logic/config/store/middleware/index.ts | 1 + .../store/reducer/currentSession.ts | 7 +- src/logic/safe/hooks/useLoadSafe.tsx | 4 +- .../store/actions/__tests__/fetchSafe.test.ts | 170 --------------- .../__tests__/transactionHelpers.test.ts | 85 -------- .../store/actions/__tests__/utils.test.ts | 195 ------------------ src/logic/safe/store/actions/fetchSafe.ts | 2 +- .../store/actions/loadSafesFromStorage.ts | 1 - src/logic/safe/store/selectors/index.ts | 7 + .../tokens/store/actions/fetchSafeTokens.ts | 19 +- src/logic/tokens/store/model/token.ts | 2 + src/pages/Assets/Tokens/ManageTokenPopup.tsx | 20 +- .../Avanced/Custom Transaction/index.tsx | 1 - src/routes/routes.ts | 1 - src/utils/safeUtils/selector.ts | 1 + 21 files changed, 103 insertions(+), 511 deletions(-) delete mode 100644 src/logic/safe/store/actions/__tests__/fetchSafe.test.ts delete mode 100644 src/logic/safe/store/actions/__tests__/transactionHelpers.test.ts delete mode 100644 src/logic/safe/store/actions/__tests__/utils.test.ts diff --git a/src/App/index.tsx b/src/App/index.tsx index 1d968cb743..61522817c2 100644 --- a/src/App/index.tsx +++ b/src/App/index.tsx @@ -26,8 +26,6 @@ import { grantedSelector } from 'src/utils/safeUtils/selector' import { ConnectWalletModal } from 'src/components/ConnectWalletModal' import { useSidebarItems } from 'src/layout/Sidebar/useSidebarItems' import useAddressBookSync from 'src/logic/addressBook/hooks/useAddressBookSync' -import loadCurrentSessionFromStorage from 'src/logic/currentSession/store/actions/loadCurrentSessionFromStorage' -import loadSafesFromStorage from 'src/logic/safe/store/actions/loadSafesFromStorage' import { extractSafeAddress, extractSafeId } from 'src/routes/routes' import TermModal from './TermModal' @@ -76,16 +74,16 @@ const App: React.FC = ({ children }) => { const termContext = useContext(TermContext) const TermState = termContext?.term || false const { name: safeName, totalFiatBalance: currentSafeBalance } = useSelector(currentSafeWithNames) - const addressFromUrl = extractSafeAddress() - const safeIdFromUrl = extractSafeId() + const safeAddress = extractSafeAddress() + const safeId = extractSafeId() const { safeActionsState, onShow, onHide, showSendFunds, hideSendFunds } = useSafeActions() const { connectWalletState, onConnectWalletShow, onConnectWalletHide } = useConnectWallet() const currentCurrency = useSelector(currentCurrencySelector) const granted = useSelector(grantedSelector) const sidebarItems = useSidebarItems() const dispatch = useDispatch() - useLoadSafe(addressFromUrl, safeIdFromUrl) // load initially - useSafeScheduledUpdates(addressFromUrl, safeIdFromUrl) // load every X seconds + useLoadSafe(safeAddress, safeId) // load initially + useSafeScheduledUpdates(safeAddress, safeId) // load every X seconds useAddressBookSync() const sendFunds = safeActionsState.sendFunds @@ -101,8 +99,6 @@ const App: React.FC = ({ children }) => { } useEffect(() => { - dispatch(loadSafesFromStorage()) - dispatch(loadCurrentSessionFromStorage()) dispatch(fetchAllValidator()) }, [dispatch]) @@ -128,7 +124,7 @@ const App: React.FC = ({ children }) => { void @@ -69,6 +72,7 @@ export default function AmountInput({ type?: React.HTMLInputTypeAttribute autoFocus?: boolean placeholder?: string + token?: Token }) { const nativeCurrency = getNativeCurrency() return ( @@ -83,7 +87,7 @@ export default function AmountInput({ Max -
{nativeCurrency.symbol}
+
{token?.symbol || nativeCurrency.symbol}
), }} diff --git a/src/components/Input/Token/index.tsx b/src/components/Input/Token/index.tsx index 15792f28bb..9f03e275fd 100644 --- a/src/components/Input/Token/index.tsx +++ b/src/components/Input/Token/index.tsx @@ -1,10 +1,9 @@ -import Select, { IOption } from 'src/components/Input/Select' -import { Token } from 'src/logic/tokens/store/model/token' -import styled from 'styled-components' import MenuItem from '@material-ui/core/MenuItem' import { useSelector } from 'react-redux' +import Select, { IOption } from 'src/components/Input/Select' +import { Token } from 'src/logic/tokens/store/model/token' import { extendedSafeTokensSelector } from 'src/utils/safeUtils/selector' -import { List } from 'immutable' +import styled from 'styled-components' const MenuItemWrapper = styled.div` display: flex; @@ -21,14 +20,14 @@ export default function TokenSelect({ selectedToken, setSelectedToken, disabled const tokenOptions: IOption[] = tokenList.map((token: Token) => ({ value: token.address, label: token.name, - })) as unknown as IOption[] + })) as IOption[] return ( setRowPerPage(v)} + /> + + + setPage(v)} + count={Math.ceil(totalCount / rowPerPage)} + shape="rounded" + /> + + )} diff --git a/src/pages/AddressBook/index.tsx b/src/pages/AddressBook/index.tsx index b9fda78f55..e16ad2b1eb 100644 --- a/src/pages/AddressBook/index.tsx +++ b/src/pages/AddressBook/index.tsx @@ -1,4 +1,4 @@ -import { FixedIcon, Icon, Menu, Text } from '@aura/safe-react-components' +import { FixedIcon, Icon, Menu } from '@aura/safe-react-components' import { makeStyles } from '@material-ui/core/styles' import TableCell from '@material-ui/core/TableCell' import TableContainer from '@material-ui/core/TableContainer' @@ -9,9 +9,13 @@ import { useDispatch, useSelector } from 'react-redux' import BreadcrumbIcon from 'src/assets/icons/BookBookmark.svg' import { useHistory } from 'react-router' +import ArrowDown from 'src/assets/icons/ArrowLineDown.svg' +import ArrowUp from 'src/assets/icons/ArrowLineUp.svg' +import Plus from 'src/assets/icons/Plus.svg' import Breadcrumb from 'src/components/Breadcrumb' -import ButtonGradient from 'src/components/ButtonGradient' +import { OutlinedButton, OutlinedNeutralButton } from 'src/components/Button' import ButtonHelper from 'src/components/ButtonHelper' +import Gap from 'src/components/Gap' import Block from 'src/components/layout/Block' import Col from 'src/components/layout/Col' import Paragraph from 'src/components/layout/Paragraph' @@ -31,16 +35,11 @@ import { CreateEditEntryModal } from 'src/pages/AddressBook/CreateEditEntryModal import { DeleteEntryModal } from 'src/pages/AddressBook/DeleteEntryModal' import { ExportEntriesModal } from 'src/pages/AddressBook/ExportEntriesModal' import SendModal from 'src/routes/safe/components/Balances/SendModal' -import { grantedSelector } from 'src/utils/safeUtils/selector' import { SAFE_EVENTS, useAnalytics } from 'src/utils/googleAnalytics' import { isValidAddress } from 'src/utils/isValidAddress' +import { grantedSelector } from 'src/utils/safeUtils/selector' import ImportEntriesModal from './ImportEntriesModal' -import { StyledButtonLink, styles } from './style' -import ArrowDown from 'src/assets/icons/ArrowLineDown.svg' -import ArrowUp from 'src/assets/icons/ArrowLineUp.svg' -import Plus from 'src/assets/icons/Plus.svg' -import Gap from 'src/components/Gap' -import { OutlinedButton, OutlinedNeutralButton } from 'src/components/Button' +import { styles } from './style' const useStyles = makeStyles(styles) interface AddressBookSelectedEntry extends AddressBookEntry { diff --git a/src/pages/Assets/Tokens/index.tsx b/src/pages/Assets/Tokens/index.tsx index d499b05634..1477537192 100644 --- a/src/pages/Assets/Tokens/index.tsx +++ b/src/pages/Assets/Tokens/index.tsx @@ -11,6 +11,7 @@ import sendIcon from 'src/assets/icons/ArrowUpRight.svg' import ManageTokenPopup from './ManageTokenPopup' import SearchInput from 'src/components/Input/Search' import { currentSafeWithNames } from 'src/logic/safe/store/selectors' +import { Token } from 'src/logic/tokens/store/model/token' const Wrap = styled.div` background: ${(props) => props.theme.backgroundPrimary}; border-radius: 8px; @@ -65,7 +66,7 @@ const TokenType = styled.div` function Tokens(props): ReactElement { const [open, setOpen] = useState(false) const [manageTokenPopupOpen, setManageTokenPopupOpen] = useState(false) - const [selectedToken, setSelectedToken] = useState(undefined) + const [selectedToken, setSelectedToken] = useState('') const safeTokens: any = useSelector(extendedSafeTokensSelector) const { coinConfig, address } = useSelector(currentSafeWithNames) @@ -80,7 +81,7 @@ function Tokens(props): ReactElement { - + {safeTokens .filter((token) => { return ( @@ -90,12 +91,12 @@ function Tokens(props): ReactElement { })?.enable ) }) - .map((token, index) => { + .map((token: Token, index: number) => { return ( - + {token.name || 'Unkonwn token'} @@ -109,7 +110,7 @@ function Tokens(props): ReactElement { className="small" onClick={() => { setOpen(true) - setSelectedToken(token.address) + setSelectedToken(token?.address) }} > From 825d1bdfd821a0bfecaf62eb09d313e290a76c60 Mon Sep 17 00:00:00 2001 From: HoangNDM6 Date: Tue, 30 May 2023 11:11:12 +0700 Subject: [PATCH 20/69] update funds --- src/components/Input/AmountInput/index.tsx | 6 +- src/components/JsonschemaForm/FundForm.tsx | 111 ++++++++++++++++++ src/components/JsonschemaForm/index.tsx | 51 +++++++- .../ContractInteraction/Contract.tsx | 9 +- 4 files changed, 170 insertions(+), 7 deletions(-) create mode 100644 src/components/JsonschemaForm/FundForm.tsx diff --git a/src/components/Input/AmountInput/index.tsx b/src/components/Input/AmountInput/index.tsx index 400fc27950..0756562db7 100644 --- a/src/components/Input/AmountInput/index.tsx +++ b/src/components/Input/AmountInput/index.tsx @@ -65,14 +65,16 @@ export default function AmountInput({ handleMax, placeholder = 'Amount', token, + endAdornment = true, }: { value: any onChange: (value: string) => void - handleMax: () => void + handleMax?: () => void type?: React.HTMLInputTypeAttribute autoFocus?: boolean placeholder?: string token?: Token + endAdornment?: boolean }) { const nativeCurrency = getNativeCurrency() return ( @@ -82,7 +84,7 @@ export default function AmountInput({ type={type} placeholder={placeholder} InputProps={{ - endAdornment: ( + endAdornment: endAdornment && ( Max diff --git a/src/components/JsonschemaForm/FundForm.tsx b/src/components/JsonschemaForm/FundForm.tsx new file mode 100644 index 0000000000..7a6bb86552 --- /dev/null +++ b/src/components/JsonschemaForm/FundForm.tsx @@ -0,0 +1,111 @@ +import { useState } from 'react' +import Select, { IOption } from '../Input/Select' +import styled from 'styled-components' +import { useSelector } from 'react-redux' +import { extendedSafeTokensSelector } from 'src/utils/safeUtils/selector' +import { MenuItem } from '@material-ui/core' +import { Token } from 'src/logic/tokens/store/model/token' +import AmountInput from '../Input/AmountInput' +import { FilledButton } from '../Button' + +const MenuItemWrapper = styled.div` + display: flex; + align-items: center; + img { + width: 20px; + height: 20px; + margin-right: 8px; + } +` + +const FormWrapper = styled.div` + display: flex; + justify-content: space-between; +` +const FormItemWrapper = styled.div` + width: 30%; + display: flex; + align-items: center; +` + +const FormLabel = styled.div` + font-weight: 400; + font-size: 16px; + line-height: 20px; + margin-right: 8px; +` +export type IFund = { + id: number + denom: string + amount: string +} + +type IFundFormProps = { + fund: IFund + onDelete: (id: number) => void + onSelectToken: (token: string) => void + selectedTokens: string[] +} + +const FundForm = ({ fund, selectedTokens, onDelete, onSelectToken }: IFundFormProps) => { + const tokenList = useSelector(extendedSafeTokensSelector) as unknown as Token[] + const filteredTokenList = tokenList.filter((token) => !selectedTokens.includes(token.denom)) + const tokenOptions: IOption[] = tokenList.map((token: Token) => ({ + value: token.denom, + label: token.name, + })) + const [selectedToken, setSelectedToken] = useState('') + const [amount, setAmount] = useState('') + + const handleDenomChange = (token: string) => { + fund.denom = token + onSelectToken(token) + setSelectedToken(token) + } + + const handleAmountChange = (value) => { + fund.amount = value + setAmount(value) + } + + return ( + + + Token: + + + + Amount: + + + onDelete(fund.id)}>Delete + + ) +} + +export default FundForm diff --git a/src/components/JsonschemaForm/index.tsx b/src/components/JsonschemaForm/index.tsx index 3fa47cecc5..0a16f71f99 100644 --- a/src/components/JsonschemaForm/index.tsx +++ b/src/components/JsonschemaForm/index.tsx @@ -1,10 +1,12 @@ +import { Box } from '@material-ui/core' import { Validator } from 'jsonschema' -import { ReactElement } from 'react' +import { ReactElement, useState } from 'react' import { useDispatch } from 'react-redux' import styled from 'styled-components' +import { FilledButton } from '../Button' import Field from './Field' +import FundForm, { IFund } from './FundForm' import { makeSchemaInput } from './utils' -import TextField from '../Input/TextField' const Wrap = styled.div` margin-top: 32px; .title { @@ -35,6 +37,9 @@ const Wrap = styled.div` } } ` + +let rowId = 1 + function JsonschemaForm({ schema, formData, @@ -47,6 +52,7 @@ function JsonschemaForm({ setFunds, }): ReactElement { const dispatch = useDispatch() + const [selectedTokens, setSelectedTokens] = useState([]) const jsValidator = new Validator() if (!schema) return <> jsValidator.addSchema(schema) @@ -59,6 +65,32 @@ function JsonschemaForm({ return '' } + + const handleAddFund = () => { + const newFund = { id: rowId, denom: '', amount: '' } + const newFunds = [...funds, newFund] + setFunds(newFunds) + rowId++ + } + + const handleDeleteFund = (id: number) => { + const updatedFunds = funds.filter((fund) => fund.id !== id) + setFunds(updatedFunds) + const updatedSelectedTokens = selectedTokens.filter((token) => token !== getDenomById(id)) + setSelectedTokens(updatedSelectedTokens) + } + + const getDenomById = (id: number) => { + const fund = funds.find((fund: IFund) => fund.id === id) + return fund ? fund.denom : '' + } + + const handleSelectToken = (denom: string) => { + const updatedSelectedTokens = [...selectedTokens] + updatedSelectedTokens.push(denom) + setSelectedTokens(updatedSelectedTokens) + } + return (
Function List
@@ -112,7 +144,20 @@ function JsonschemaForm({ }} /> ))} - +
Funds
+ {funds.map((fund: IFund) => ( +
+ +
+ ))} + + Add Fund +
) diff --git a/src/pages/SmartContract/ContractInteraction/Contract.tsx b/src/pages/SmartContract/ContractInteraction/Contract.tsx index ae0e81497a..507def7bcf 100644 --- a/src/pages/SmartContract/ContractInteraction/Contract.tsx +++ b/src/pages/SmartContract/ContractInteraction/Contract.tsx @@ -12,6 +12,7 @@ import ReviewPopup from './ReviewPopup' import { Validator } from 'jsonschema' import { makeSchemaInput } from 'src/components/JsonschemaForm/utils' import Loader from 'src/components/Loader' +import { IFund } from 'src/components/JsonschemaForm/FundForm' const Wrap = styled.div` .preview-button { @@ -33,7 +34,7 @@ function Contract({ contractData }): ReactElement { const [shouldCheck, setShouldCheck] = useState(false) const [activeFunction, setActiveFunction] = useState(0) const [selectedFunction, setSelectedFunction] = useState('') - const [funds, setFunds] = useState('') + const [funds, setFunds] = useState([{ id: 0, denom: '', amount: '' }]) const [schema, setSchema] = useState() const [loading, setLoading] = useState(false) const preview = async () => { @@ -133,7 +134,11 @@ function Contract({ contractData }): ReactElement { setOpen={setOpen} gasUsed={Math.round(gasUsed * 1.3)} data={formData} - contractData={{ ...contractData, selectedFunction: selectedFunction, funds }} + contractData={{ + ...contractData, + selectedFunction: selectedFunction, + funds: funds.map(({ id, ...rest }) => rest), + }} /> ) From 73dfa78db4d90c1f72c32c37a61d3d20abfb2b03 Mon Sep 17 00:00:00 2001 From: HoangNDM6 Date: Tue, 30 May 2023 11:19:51 +0700 Subject: [PATCH 21/69] filter funds --- src/pages/SmartContract/ContractInteraction/Contract.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/SmartContract/ContractInteraction/Contract.tsx b/src/pages/SmartContract/ContractInteraction/Contract.tsx index 507def7bcf..ec9cc61274 100644 --- a/src/pages/SmartContract/ContractInteraction/Contract.tsx +++ b/src/pages/SmartContract/ContractInteraction/Contract.tsx @@ -137,7 +137,7 @@ function Contract({ contractData }): ReactElement { contractData={{ ...contractData, selectedFunction: selectedFunction, - funds: funds.map(({ id, ...rest }) => rest), + funds: funds.filter((fund) => fund.denom !== '').map(({ id, ...rest }) => rest), }} /> From 5a7eb0f32b4f03eaa273b7d6e87a58bde9adb5f7 Mon Sep 17 00:00:00 2001 From: HoangNDM6 Date: Tue, 30 May 2023 13:33:22 +0700 Subject: [PATCH 22/69] change icon button --- src/assets/icons/ic_trash.svg | 6 ++++++ src/components/JsonschemaForm/FundForm.tsx | 16 ++++++++++------ src/components/JsonschemaForm/index.tsx | 10 +++++++--- 3 files changed, 23 insertions(+), 9 deletions(-) create mode 100644 src/assets/icons/ic_trash.svg diff --git a/src/assets/icons/ic_trash.svg b/src/assets/icons/ic_trash.svg new file mode 100644 index 0000000000..33dade7f9e --- /dev/null +++ b/src/assets/icons/ic_trash.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/components/JsonschemaForm/FundForm.tsx b/src/components/JsonschemaForm/FundForm.tsx index 7a6bb86552..340b101d1d 100644 --- a/src/components/JsonschemaForm/FundForm.tsx +++ b/src/components/JsonschemaForm/FundForm.tsx @@ -1,12 +1,13 @@ +import { MenuItem } from '@material-ui/core' import { useState } from 'react' -import Select, { IOption } from '../Input/Select' -import styled from 'styled-components' import { useSelector } from 'react-redux' -import { extendedSafeTokensSelector } from 'src/utils/safeUtils/selector' -import { MenuItem } from '@material-ui/core' import { Token } from 'src/logic/tokens/store/model/token' +import { extendedSafeTokensSelector } from 'src/utils/safeUtils/selector' +import styled from 'styled-components' +import ic_trash from '../../assets/icons/ic_trash.svg' +import ButtonHelper from '../ButtonHelper' import AmountInput from '../Input/AmountInput' -import { FilledButton } from '../Button' +import Select, { IOption } from '../Input/Select' const MenuItemWrapper = styled.div` display: flex; @@ -20,6 +21,7 @@ const MenuItemWrapper = styled.div` const FormWrapper = styled.div` display: flex; + align-items: center; justify-content: space-between; ` const FormItemWrapper = styled.div` @@ -103,7 +105,9 @@ const FundForm = ({ fund, selectedTokens, onDelete, onSelectToken }: IFundFormPr Amount: - onDelete(fund.id)}>Delete + onDelete(fund.id)}> + Trash Icon + ) } diff --git a/src/components/JsonschemaForm/index.tsx b/src/components/JsonschemaForm/index.tsx index 0a16f71f99..acfcc64e43 100644 --- a/src/components/JsonschemaForm/index.tsx +++ b/src/components/JsonschemaForm/index.tsx @@ -1,9 +1,9 @@ -import { Box } from '@material-ui/core' +import { Box, Button } from '@material-ui/core' import { Validator } from 'jsonschema' import { ReactElement, useState } from 'react' import { useDispatch } from 'react-redux' import styled from 'styled-components' -import { FilledButton } from '../Button' +import Paragraph from '../layout/Paragraph' import Field from './Field' import FundForm, { IFund } from './FundForm' import { makeSchemaInput } from './utils' @@ -156,7 +156,11 @@ function JsonschemaForm({ ))} - Add Fund + From 63d0db44092bbcf94badaf7ca40a83891556a95f Mon Sep 17 00:00:00 2001 From: imhson Date: Tue, 30 May 2023 13:42:01 +0700 Subject: [PATCH 23/69] remove input when changen function --- src/components/JsonschemaForm/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/JsonschemaForm/index.tsx b/src/components/JsonschemaForm/index.tsx index 3fa47cecc5..fd6192f962 100644 --- a/src/components/JsonschemaForm/index.tsx +++ b/src/components/JsonschemaForm/index.tsx @@ -85,7 +85,7 @@ function JsonschemaForm({ value={ typeof formData[field.fieldName] == 'object' ? JSON.stringify(formData[field.fieldName]) - : formData[field.fieldName] + : formData[field.fieldName] || '' } errorMsg={shouldCheck ? validateField(field) : ''} onChange={(value) => { From 3b755e67c7bc6ccce908f21d745365e6ef159419 Mon Sep 17 00:00:00 2001 From: HoangNDM6 Date: Tue, 30 May 2023 14:37:55 +0700 Subject: [PATCH 24/69] update field funds --- src/pages/SmartContract/ContractInteraction/ReviewPopup.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/SmartContract/ContractInteraction/ReviewPopup.tsx b/src/pages/SmartContract/ContractInteraction/ReviewPopup.tsx index 39d9ec1afb..29fb2a5575 100644 --- a/src/pages/SmartContract/ContractInteraction/ReviewPopup.tsx +++ b/src/pages/SmartContract/ContractInteraction/ReviewPopup.tsx @@ -50,7 +50,7 @@ export default function ReviewPopup({ open, setOpen, gasUsed, data, contractData value: { contract: contractData.contractAddress, sender: safeAddress, - funds: contractData.funds ? JSON.parse(contractData.funds) : [], + funds: contractData.funds ?? [], msg: { [contractData.selectedFunction]: data, }, From edd89b34541d988274b5bc67d6ee4d5f6ff74074 Mon Sep 17 00:00:00 2001 From: HoangNDM6 Date: Wed, 31 May 2023 11:40:25 +0700 Subject: [PATCH 25/69] update input amount funds --- src/components/Input/AmountInput/index.tsx | 6 +- src/components/JsonschemaForm/FundForm.tsx | 119 ++++++++++++++------- 2 files changed, 81 insertions(+), 44 deletions(-) diff --git a/src/components/Input/AmountInput/index.tsx b/src/components/Input/AmountInput/index.tsx index 0756562db7..400fc27950 100644 --- a/src/components/Input/AmountInput/index.tsx +++ b/src/components/Input/AmountInput/index.tsx @@ -65,16 +65,14 @@ export default function AmountInput({ handleMax, placeholder = 'Amount', token, - endAdornment = true, }: { value: any onChange: (value: string) => void - handleMax?: () => void + handleMax: () => void type?: React.HTMLInputTypeAttribute autoFocus?: boolean placeholder?: string token?: Token - endAdornment?: boolean }) { const nativeCurrency = getNativeCurrency() return ( @@ -84,7 +82,7 @@ export default function AmountInput({ type={type} placeholder={placeholder} InputProps={{ - endAdornment: endAdornment && ( + endAdornment: ( Max diff --git a/src/components/JsonschemaForm/FundForm.tsx b/src/components/JsonschemaForm/FundForm.tsx index 340b101d1d..80fd964a53 100644 --- a/src/components/JsonschemaForm/FundForm.tsx +++ b/src/components/JsonschemaForm/FundForm.tsx @@ -1,5 +1,5 @@ import { MenuItem } from '@material-ui/core' -import { useState } from 'react' +import { useEffect, useState } from 'react' import { useSelector } from 'react-redux' import { Token } from 'src/logic/tokens/store/model/token' import { extendedSafeTokensSelector } from 'src/utils/safeUtils/selector' @@ -25,7 +25,7 @@ const FormWrapper = styled.div` justify-content: space-between; ` const FormItemWrapper = styled.div` - width: 30%; + width: 40%; display: flex; align-items: center; ` @@ -36,6 +36,17 @@ const FormLabel = styled.div` line-height: 20px; margin-right: 8px; ` + +const ErrorMsg = styled.div` + color: #bf2525; + font-size: 12px; + position: absolute; + left: 55%; +` +const Wrap = styled.div` + position: relative; + width: 100%; +` export type IFund = { id: number denom: string @@ -56,13 +67,34 @@ const FundForm = ({ fund, selectedTokens, onDelete, onSelectToken }: IFundFormPr value: token.denom, label: token.name, })) - const [selectedToken, setSelectedToken] = useState('') + + const [selectedToken, setSelectedToken] = useState(undefined) + const [token, setToken] = useState('') const [amount, setAmount] = useState('') + const [amountValidateMsg, setAmountValidateMsg] = useState('') + + useEffect(() => { + setAmountValidateMsg('') + if (selectedToken?.address) { + const tokenbalance = tokenList.find((t) => t.denom == selectedToken?.denom)?.balance?.tokenBalance + if (tokenbalance && +amount > +tokenbalance) { + setAmountValidateMsg('Given amount is greater than available balance.') + } + } + }, [amount, selectedToken?.address]) const handleDenomChange = (token: string) => { fund.denom = token onSelectToken(token) - setSelectedToken(token) + setToken(token) + const selectedToken = filteredTokenList.find((e) => e.denom === token) + setSelectedToken(selectedToken) + } + + const setMaxAmount = () => { + if (selectedToken) { + setAmount(selectedToken?.balance?.tokenBalance || '') + } } const handleAmountChange = (value) => { @@ -71,44 +103,51 @@ const FundForm = ({ fund, selectedTokens, onDelete, onSelectToken }: IFundFormPr } return ( - - - Token: - { + const selectedToken = tokenList.find((token) => token.denom == value) + return selectedToken ? ( - {token.name} - {token.name} + {selectedToken.name} + {`${selectedToken.balance.tokenBalance} ${selectedToken.symbol}`} - - ) - })} - - - - Amount: - - - onDelete(fund.id)}> - Trash Icon - - + ) : null + }} + > + {filteredTokenList.map((token: Token, index: any) => { + return ( + + + {token.name} + {`${token.balance.tokenBalance} ${token.symbol}`} + + + ) + })} + + + + Amount: + + + onDelete(fund.id)}> + Trash Icon + + + {amountValidateMsg && ( + + {amountValidateMsg} + + )} + ) } From 930eeaec0dec8f74bb6b2bf5ab407c77f61c090a Mon Sep 17 00:00:00 2001 From: HoangNDM6 Date: Wed, 31 May 2023 13:48:14 +0700 Subject: [PATCH 26/69] update input amount funds --- src/components/JsonschemaForm/FundForm.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/components/JsonschemaForm/FundForm.tsx b/src/components/JsonschemaForm/FundForm.tsx index 80fd964a53..9837e70774 100644 --- a/src/components/JsonschemaForm/FundForm.tsx +++ b/src/components/JsonschemaForm/FundForm.tsx @@ -69,13 +69,12 @@ const FundForm = ({ fund, selectedTokens, onDelete, onSelectToken }: IFundFormPr })) const [selectedToken, setSelectedToken] = useState(undefined) - const [token, setToken] = useState('') const [amount, setAmount] = useState('') const [amountValidateMsg, setAmountValidateMsg] = useState('') useEffect(() => { setAmountValidateMsg('') - if (selectedToken?.address) { + if (selectedToken?.denom) { const tokenbalance = tokenList.find((t) => t.denom == selectedToken?.denom)?.balance?.tokenBalance if (tokenbalance && +amount > +tokenbalance) { setAmountValidateMsg('Given amount is greater than available balance.') @@ -86,13 +85,13 @@ const FundForm = ({ fund, selectedTokens, onDelete, onSelectToken }: IFundFormPr const handleDenomChange = (token: string) => { fund.denom = token onSelectToken(token) - setToken(token) const selectedToken = filteredTokenList.find((e) => e.denom === token) setSelectedToken(selectedToken) } const setMaxAmount = () => { if (selectedToken) { + fund.amount = selectedToken?.balance?.tokenBalance || '' setAmount(selectedToken?.balance?.tokenBalance || '') } } @@ -109,7 +108,7 @@ const FundForm = ({ fund, selectedTokens, onDelete, onSelectToken }: IFundFormPr Token: { const selectedToken = tokenList.find((token) => token.denom == value) return selectedToken ? ( @@ -132,11 +120,17 @@ const FundForm = ({ fund, selectedTokens, onDelete, onSelectToken }: IFundFormPr ) })} - - - Amount: - - + + + + onDelete(fund.id)}> Trash Icon diff --git a/src/components/JsonschemaForm/index.tsx b/src/components/JsonschemaForm/index.tsx index 0a79769bae..2bc92db1f3 100644 --- a/src/components/JsonschemaForm/index.tsx +++ b/src/components/JsonschemaForm/index.tsx @@ -1,4 +1,4 @@ -import { Box, Button } from '@material-ui/core' +import { Button } from '@material-ui/core' import { Validator } from 'jsonschema' import { ReactElement, useState } from 'react' import { useDispatch } from 'react-redux' @@ -38,6 +38,13 @@ const Wrap = styled.div` } ` +const Title = styled.div` + font-weight: 600; + font-size: 20px; + line-height: 24px; + margin-top: 16px; +` + let rowId = 1 function JsonschemaForm({ @@ -144,7 +151,7 @@ function JsonschemaForm({ }} /> ))} -
Funds
+ Transaction funds {funds.map((fund: IFund) => (
))} - - - + ) From 34d02e2fec1f724a2807d2dd23fad7033c7f64e5 Mon Sep 17 00:00:00 2001 From: HoangNDM6 Date: Wed, 31 May 2023 15:13:20 +0700 Subject: [PATCH 28/69] update UI funds --- src/components/Input/AmountInput/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/Input/AmountInput/index.tsx b/src/components/Input/AmountInput/index.tsx index 6c169d25de..bbcd8195aa 100644 --- a/src/components/Input/AmountInput/index.tsx +++ b/src/components/Input/AmountInput/index.tsx @@ -81,7 +81,6 @@ export default function AmountInput({ invalid?: boolean }) { const nativeCurrency = getNativeCurrency() - console.log(invalid, 'invalidinvalid') return ( Date: Wed, 31 May 2023 16:08:32 +0700 Subject: [PATCH 29/69] convert amount --- src/components/JsonschemaForm/FundForm.tsx | 2 ++ src/pages/SmartContract/ContractInteraction/Contract.tsx | 7 +++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/components/JsonschemaForm/FundForm.tsx b/src/components/JsonschemaForm/FundForm.tsx index 0957d1ee0d..ec368f89fd 100644 --- a/src/components/JsonschemaForm/FundForm.tsx +++ b/src/components/JsonschemaForm/FundForm.tsx @@ -47,6 +47,7 @@ export type IFund = { id: number denom: string amount: string + tokenDecimal: string | number } type IFundFormProps = { @@ -82,6 +83,7 @@ const FundForm = ({ fund, selectedTokens, onDelete, onSelectToken }: IFundFormPr fund.denom = token onSelectToken(token) const selectedToken = filteredTokenList.find((e) => e.denom === token) + fund.tokenDecimal = selectedToken ? selectedToken.decimals : 0 setSelectedToken(selectedToken) } diff --git a/src/pages/SmartContract/ContractInteraction/Contract.tsx b/src/pages/SmartContract/ContractInteraction/Contract.tsx index ec9cc61274..d24cd5faf5 100644 --- a/src/pages/SmartContract/ContractInteraction/Contract.tsx +++ b/src/pages/SmartContract/ContractInteraction/Contract.tsx @@ -13,6 +13,7 @@ import { Validator } from 'jsonschema' import { makeSchemaInput } from 'src/components/JsonschemaForm/utils' import Loader from 'src/components/Loader' import { IFund } from 'src/components/JsonschemaForm/FundForm' +import { convertAmount } from 'src/utils' const Wrap = styled.div` .preview-button { @@ -34,7 +35,7 @@ function Contract({ contractData }): ReactElement { const [shouldCheck, setShouldCheck] = useState(false) const [activeFunction, setActiveFunction] = useState(0) const [selectedFunction, setSelectedFunction] = useState('') - const [funds, setFunds] = useState([{ id: 0, denom: '', amount: '' }]) + const [funds, setFunds] = useState([{ id: 0, denom: '', amount: '', tokenDecimal: '' }]) const [schema, setSchema] = useState() const [loading, setLoading] = useState(false) const preview = async () => { @@ -137,7 +138,9 @@ function Contract({ contractData }): ReactElement { contractData={{ ...contractData, selectedFunction: selectedFunction, - funds: funds.filter((fund) => fund.denom !== '').map(({ id, ...rest }) => rest), + funds: funds + .filter((fund) => fund.denom !== '') + .map((e) => ({ denom: e.denom, amount: convertAmount(+e.amount, true, +e.tokenDecimal) })), }} /> From 0650e91eb549349e83f8489b3fad8d51bc5bb295 Mon Sep 17 00:00:00 2001 From: HoangNDM6 Date: Thu, 1 Jun 2023 16:52:10 +0700 Subject: [PATCH 30/69] format amount --- src/components/Input/AmountInput/index.tsx | 13 ++-- src/components/JsonschemaForm/FundForm.tsx | 64 +++++++++++++------ src/components/JsonschemaForm/index.tsx | 16 ++++- src/components/TxComponents/Amount.tsx | 56 ++++++++++++++-- .../ContractInteraction/Contract.tsx | 14 +++- .../ContractInteraction/ReviewPopup.tsx | 20 +++++- 6 files changed, 146 insertions(+), 37 deletions(-) diff --git a/src/components/Input/AmountInput/index.tsx b/src/components/Input/AmountInput/index.tsx index bbcd8195aa..7f37c27660 100644 --- a/src/components/Input/AmountInput/index.tsx +++ b/src/components/Input/AmountInput/index.tsx @@ -8,7 +8,7 @@ import { colorLinear } from 'src/theme/variables' import { formatNumber, isNumberKeyPress } from 'src/utils' import styled from 'styled-components' -const StyledTextField = styled(MuiTextField)<{ invalid?: boolean }>` +const StyledTextField = styled(MuiTextField)<{ invalid?: string }>` width: 100%; > label { z-index: 1; @@ -24,7 +24,7 @@ const StyledTextField = styled(MuiTextField)<{ invalid?: boolean }>` } > div { background: #24262e; - ${({ invalid }) => (invalid ? { border: '1px solid #d5625e' } : { border: '1px solid #494c58' })} + ${({ invalid }) => (invalid === 'true' ? { border: '1px solid #d5625e' } : { border: '1px solid #494c58' })} color: #fff; border-radius: 8px; overflow: hidden; @@ -35,7 +35,7 @@ const StyledTextField = styled(MuiTextField)<{ invalid?: boolean }>` } > div.Mui-focused { background: linear-gradient(#24262e, #24262e) padding-box, ${colorLinear} border-box; - ${({ invalid }) => (invalid ? { border: '1px solid #d5625e' } : { border: '1px solid transparent' })} + ${({ invalid }) => (invalid === 'true' ? { border: '1px solid #d5625e' } : { border: '1px solid transparent' })} } input { color: #fff; @@ -65,9 +65,8 @@ export default function AmountInput({ handleMax, placeholder = 'Amount', token, - disabled = false, showBtnMax = true, - invalid = false, + invalid = 'false', }: { value: any onChange: (value: string) => void @@ -76,16 +75,14 @@ export default function AmountInput({ autoFocus?: boolean placeholder?: string token?: Token - disabled?: boolean showBtnMax?: boolean - invalid?: boolean + invalid?: string }) { const nativeCurrency = getNativeCurrency() return ( void onSelectToken: (token: string) => void + onDeselectToken: (token: string, preToken: string) => void selectedTokens: string[] + onChangeAmount: (isError: boolean) => void } - -const FundForm = ({ fund, selectedTokens, onDelete, onSelectToken }: IFundFormProps) => { +let preToken: Record = {} +const FundForm = ({ + fund, + selectedTokens, + onDelete, + onSelectToken, + onChangeAmount, + onDeselectToken, +}: IFundFormProps) => { const tokenList = useSelector(extendedSafeTokensSelector) as unknown as Token[] const filteredTokenList = tokenList.filter((token) => !selectedTokens.includes(token.denom)) const tokenOptions: IOption[] = tokenList.map((token: Token) => ({ @@ -67,7 +79,7 @@ const FundForm = ({ fund, selectedTokens, onDelete, onSelectToken }: IFundFormPr const [selectedToken, setSelectedToken] = useState(undefined) const [amount, setAmount] = useState('') - const [amountValidateMsg, setAmountValidateMsg] = useState('') + const [amountValidateMsg, setAmountValidateMsg] = useState('') useEffect(() => { setAmountValidateMsg('') @@ -77,17 +89,30 @@ const FundForm = ({ fund, selectedTokens, onDelete, onSelectToken }: IFundFormPr setAmountValidateMsg('Insufficient funds.') } } - }, [amount, selectedToken?.address]) + }, [amount, selectedToken?.denom]) + + useEffect(() => { + onChangeAmount(amountValidateMsg !== '') + }, [amountValidateMsg]) const handleDenomChange = (token: string) => { fund.denom = token onSelectToken(token) + + if (preToken[fund.id] !== undefined && token !== preToken[fund.id]) { + onDeselectToken(token, preToken[fund.id]) + } + preToken[fund.id] = token + const selectedToken = filteredTokenList.find((e) => e.denom === token) - fund.tokenDecimal = selectedToken ? selectedToken.decimals : 0 + fund.tokenDecimal = selectedToken?.decimals ?? 0 + fund.logoUri = selectedToken?.logoUri ?? '' + fund.type = selectedToken?.type ?? '' + fund.symbol = selectedToken?.symbol ?? '' setSelectedToken(selectedToken) } - const handleAmountChange = (value) => { + const handleAmountChange = (value: string) => { fund.amount = value setAmount(value) } @@ -123,18 +148,21 @@ const FundForm = ({ fund, selectedTokens, onDelete, onSelectToken }: IFundFormPr })} - - - + {selectedToken ? ( + + + + ) : ( + <> + )} onDelete(fund.id)}> - Trash Icon + } title="Delete"> {amountValidateMsg && ( diff --git a/src/components/JsonschemaForm/index.tsx b/src/components/JsonschemaForm/index.tsx index 2bc92db1f3..6ddc67358e 100644 --- a/src/components/JsonschemaForm/index.tsx +++ b/src/components/JsonschemaForm/index.tsx @@ -45,7 +45,7 @@ const Title = styled.div` margin-top: 16px; ` -let rowId = 1 +let rowId = 0 function JsonschemaForm({ schema, @@ -57,6 +57,7 @@ function JsonschemaForm({ setActiveFunction, funds, setFunds, + setInvalidAmount, }): ReactElement { const dispatch = useDispatch() const [selectedTokens, setSelectedTokens] = useState([]) @@ -98,6 +99,17 @@ function JsonschemaForm({ setSelectedTokens(updatedSelectedTokens) } + const handleDeselectToken = (denom: string, preToken: string) => { + const tokens = selectedTokens.filter((e) => e !== preToken) + const updatedSelectedTokens = [...tokens] + updatedSelectedTokens.push(denom) + setSelectedTokens(updatedSelectedTokens) + } + + const handleChangeAmount = (isError: boolean) => { + setInvalidAmount(isError) + } + return (
Function List
@@ -159,6 +171,8 @@ function JsonschemaForm({ selectedTokens={selectedTokens} onDelete={handleDeleteFund} onSelectToken={handleSelectToken} + onChangeAmount={handleChangeAmount} + onDeselectToken={handleDeselectToken} /> ))} diff --git a/src/components/TxComponents/Amount.tsx b/src/components/TxComponents/Amount.tsx index 09582b42f2..6412990ecf 100644 --- a/src/components/TxComponents/Amount.tsx +++ b/src/components/TxComponents/Amount.tsx @@ -1,25 +1,73 @@ +import BigNumber from 'bignumber.js' import { getNativeCurrency } from 'src/config' import { Token } from 'src/logic/tokens/store/model/token' +import { IFund } from '../JsonschemaForm/FundForm' + +const OtherToken = ({ token, hideLogo, nativeCurrency }) => { + return ( +
+ {!hideLogo && {'nonnativeCurrencyLogoUri'}} +

+ {+token.amount} {token.symbol} +

+
+ ) +} export default function Amount({ label = 'Amount', amount = '0', token, + listTokens, hideLogo, + nativeFee = 0, }: { label?: string amount?: string token?: Token + listTokens?: IFund[] hideLogo?: boolean + nativeFee?: number }) { const nativeCurrency = getNativeCurrency() + const nativeToken = listTokens?.find((token) => token.type === 'native') + const otherToken = listTokens?.filter((token: IFund) => token.type !== 'native') + return (

{label}

-
- {!hideLogo && {'nativeCurrencyLogoUri'}} -

{amount}

-
+ {listTokens ? ( + nativeToken ? ( + <> +
+ {!hideLogo && {'nativeCurrencyLogoUri'}} +

+ {new BigNumber(nativeFee).plus(new BigNumber(+nativeToken.amount)).toString()} {nativeToken.symbol} +

+
+ {otherToken?.map((token: IFund) => ( + + ))} + + ) : ( + <> +
+ {!hideLogo && {'nativeCurrencyLogoUri'}} +

{amount}

+
+ {listTokens.map((token: IFund) => ( + + ))} + + ) + ) : ( +
+ {!hideLogo && ( + {'nativeCurrencyLogoUri'} + )} +

{amount}

+
+ )}
) } diff --git a/src/pages/SmartContract/ContractInteraction/Contract.tsx b/src/pages/SmartContract/ContractInteraction/Contract.tsx index d24cd5faf5..549a58bc0a 100644 --- a/src/pages/SmartContract/ContractInteraction/Contract.tsx +++ b/src/pages/SmartContract/ContractInteraction/Contract.tsx @@ -35,9 +35,10 @@ function Contract({ contractData }): ReactElement { const [shouldCheck, setShouldCheck] = useState(false) const [activeFunction, setActiveFunction] = useState(0) const [selectedFunction, setSelectedFunction] = useState('') - const [funds, setFunds] = useState([{ id: 0, denom: '', amount: '', tokenDecimal: '' }]) + const [funds, setFunds] = useState([]) const [schema, setSchema] = useState() const [loading, setLoading] = useState(false) + const [invalidAmount, setInvalidAmount] = useState(false) const preview = async () => { try { setLoading(true) @@ -52,7 +53,7 @@ function Contract({ contractData }): ReactElement { isError = true } }) - if (!isError) { + if (!isError && !invalidAmount) { try { const res = await simulate({ encodedMsgs: Buffer.from( @@ -124,6 +125,7 @@ function Contract({ contractData }): ReactElement { setActiveFunction={setActiveFunction} funds={funds} setFunds={setFunds} + setInvalidAmount={setInvalidAmount} />
@@ -140,7 +142,13 @@ function Contract({ contractData }): ReactElement { selectedFunction: selectedFunction, funds: funds .filter((fund) => fund.denom !== '') - .map((e) => ({ denom: e.denom, amount: convertAmount(+e.amount, true, +e.tokenDecimal) })), + .map((e) => ({ + denom: e.denom, + amount: e.amount, + logoUri: e.logoUri, + type: e.type, + symbol: e.symbol, + })), }} /> diff --git a/src/pages/SmartContract/ContractInteraction/ReviewPopup.tsx b/src/pages/SmartContract/ContractInteraction/ReviewPopup.tsx index 29fb2a5575..d11233d1f6 100644 --- a/src/pages/SmartContract/ContractInteraction/ReviewPopup.tsx +++ b/src/pages/SmartContract/ContractInteraction/ReviewPopup.tsx @@ -13,9 +13,10 @@ import { MsgTypeUrl } from 'src/logic/providers/constants/constant' import calculateGasFee from 'src/logic/providers/utils/fee' import { currentSafeWithNames } from 'src/logic/safe/store/selectors' import { extractSafeAddress } from 'src/routes/routes' -import { formatNativeCurrency } from 'src/utils' +import { convertAmount, formatNativeCurrency } from 'src/utils' import { signAndCreateTransaction } from 'src/utils/signer' import { Wrap } from './styles' +import { IFund } from 'src/components/JsonschemaForm/FundForm' export default function ReviewPopup({ open, setOpen, gasUsed, data, contractData }) { const safeAddress = extractSafeAddress() @@ -44,13 +45,21 @@ export default function ReviewPopup({ open, setOpen, gasUsed, data, contractData }, [gasUsed]) const signTransaction = async () => { + const updatedFunds = contractData.funds.map((fund: IFund) => { + const { logoUri, type, symbol, ...rest } = fund + const updatedAmount = convertAmount(+fund.amount, true, +fund.tokenDecimal) + return { + ...rest, + amount: updatedAmount, + } + }) const msgs: any = [ { typeUrl: MsgTypeUrl.ExecuteContract, value: { contract: contractData.contractAddress, sender: safeAddress, - funds: contractData.funds ?? [], + funds: updatedFunds ?? [], msg: { [contractData.selectedFunction]: data, }, @@ -107,7 +116,12 @@ export default function ReviewPopup({ open, setOpen, gasUsed, data, contractData setSequence={setSequence} /> - +
You’re about to create a transaction and will have to confirm it with your currently connected wallet.
From b896331c9040382b185bdeb7342d041f7480ab6e Mon Sep 17 00:00:00 2001 From: HoangNDM6 Date: Thu, 1 Jun 2023 17:28:34 +0700 Subject: [PATCH 31/69] fix btn add fund --- src/components/JsonschemaForm/index.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/components/JsonschemaForm/index.tsx b/src/components/JsonschemaForm/index.tsx index 6ddc67358e..2b1ddf7776 100644 --- a/src/components/JsonschemaForm/index.tsx +++ b/src/components/JsonschemaForm/index.tsx @@ -7,6 +7,9 @@ import Paragraph from '../layout/Paragraph' import Field from './Field' import FundForm, { IFund } from './FundForm' import { makeSchemaInput } from './utils' +import { OutlinedButton } from '../Button' +import Plus from '../../assets/icons/Plus.svg' + const Wrap = styled.div` margin-top: 32px; .title { @@ -176,11 +179,10 @@ function JsonschemaForm({ />
))} - + + + Add funds +
) From 53e75cad506153279d1f1e4004222e171dd15e31 Mon Sep 17 00:00:00 2001 From: HoangNDM6 Date: Mon, 5 Jun 2023 17:47:52 +0700 Subject: [PATCH 32/69] update UI funds --- src/components/JsonschemaForm/FundForm.tsx | 135 +++++---------- src/components/JsonschemaForm/index.tsx | 89 +++++----- src/components/TxComponents/Amount.tsx | 30 ++-- .../ContractInteraction/Contract.tsx | 15 +- .../ContractInteraction/ManageTokenPopup.tsx | 157 ++++++++++++++++++ 5 files changed, 260 insertions(+), 166 deletions(-) create mode 100644 src/pages/SmartContract/ContractInteraction/ManageTokenPopup.tsx diff --git a/src/components/JsonschemaForm/FundForm.tsx b/src/components/JsonschemaForm/FundForm.tsx index 615c75534b..ce2c54997a 100644 --- a/src/components/JsonschemaForm/FundForm.tsx +++ b/src/components/JsonschemaForm/FundForm.tsx @@ -1,4 +1,4 @@ -import { MenuItem, Tooltip } from '@material-ui/core' +import { Tooltip } from '@material-ui/core' import { useEffect, useState } from 'react' import { useSelector } from 'react-redux' import { Token } from 'src/logic/tokens/store/model/token' @@ -7,15 +7,18 @@ import styled from 'styled-components' import ic_trash from '../../assets/icons/ic_trash.svg' import ButtonHelper from '../ButtonHelper' import AmountInput from '../Input/AmountInput' -import Select, { IOption } from '../Input/Select' -const MenuItemWrapper = styled.div` - display: flex; +const TokenWrapper = styled.div` + background: #24262e; + border: 1px solid #494c58; + border-radius: 8px; + padding: 14px 16px; align-items: center; - img { - width: 20px; - height: 20px; - margin-right: 8px; + display: flex; + min-width: 220px; + margin-right: 20px; + > div { + text-transform: uppercase; } ` @@ -23,10 +26,6 @@ const FormWrapper = styled.div` display: flex; align-items: center; ` -const SelectWrapper = styled.div` - margin-right: 16px; - width: 35%; -` const InputWrapper = styled.div` margin-right: 23px; @@ -37,81 +36,49 @@ const ErrorMsg = styled.div` color: #d5625e; font-size: 12px; position: absolute; - left: 37%; + left: 274px; ` const Wrap = styled.div` position: relative; width: 100%; ` export type IFund = { - id: number + id: string denom: string amount: string tokenDecimal: string | number logoUri: string | null type: string symbol: string + name: string + balance: string + enabled?: boolean } type IFundFormProps = { fund: IFund - onDelete: (id: number) => void - onSelectToken: (token: string) => void - onDeselectToken: (token: string, preToken: string) => void - selectedTokens: string[] + onDelete: (id: string) => void onChangeAmount: (isError: boolean) => void } -let preToken: Record = {} -const FundForm = ({ - fund, - selectedTokens, - onDelete, - onSelectToken, - onChangeAmount, - onDeselectToken, -}: IFundFormProps) => { - const tokenList = useSelector(extendedSafeTokensSelector) as unknown as Token[] - const filteredTokenList = tokenList.filter((token) => !selectedTokens.includes(token.denom)) - const tokenOptions: IOption[] = tokenList.map((token: Token) => ({ - value: token.denom, - label: token.name, - })) - const [selectedToken, setSelectedToken] = useState(undefined) +const FundForm = ({ fund, onDelete, onChangeAmount }: IFundFormProps) => { + const tokenList = useSelector(extendedSafeTokensSelector) as unknown as Token[] + const token = tokenList.find((token) => token.denom === fund.denom) const [amount, setAmount] = useState('') const [amountValidateMsg, setAmountValidateMsg] = useState('') useEffect(() => { setAmountValidateMsg('') - if (selectedToken?.denom) { - const tokenbalance = tokenList.find((t) => t.denom == selectedToken?.denom)?.balance?.tokenBalance - if (tokenbalance && +amount > +tokenbalance) { - setAmountValidateMsg('Insufficient funds.') - } + const tokenbalance = token?.balance.tokenBalance + if (tokenbalance && +amount > +tokenbalance) { + setAmountValidateMsg('Insufficient funds.') } - }, [amount, selectedToken?.denom]) + }, [amount]) useEffect(() => { onChangeAmount(amountValidateMsg !== '') }, [amountValidateMsg]) - const handleDenomChange = (token: string) => { - fund.denom = token - onSelectToken(token) - - if (preToken[fund.id] !== undefined && token !== preToken[fund.id]) { - onDeselectToken(token, preToken[fund.id]) - } - preToken[fund.id] = token - - const selectedToken = filteredTokenList.find((e) => e.denom === token) - fund.tokenDecimal = selectedToken?.decimals ?? 0 - fund.logoUri = selectedToken?.logoUri ?? '' - fund.type = selectedToken?.type ?? '' - fund.symbol = selectedToken?.symbol ?? '' - setSelectedToken(selectedToken) - } - const handleAmountChange = (value: string) => { fund.amount = value setAmount(value) @@ -120,47 +87,19 @@ const FundForm = ({ return ( <> - - - - {selectedToken ? ( - - - - ) : ( - <> - )} + + {fund.name} +
{`${fund.balance} ${fund.symbol}`}
+
+ + + onDelete(fund.id)}> } title="Delete"> diff --git a/src/components/JsonschemaForm/index.tsx b/src/components/JsonschemaForm/index.tsx index 2b1ddf7776..4208c117cd 100644 --- a/src/components/JsonschemaForm/index.tsx +++ b/src/components/JsonschemaForm/index.tsx @@ -1,14 +1,15 @@ -import { Button } from '@material-ui/core' import { Validator } from 'jsonschema' -import { ReactElement, useState } from 'react' -import { useDispatch } from 'react-redux' +import { ReactElement, useEffect, useState } from 'react' +import { useSelector } from 'react-redux' +import { Token } from 'src/logic/tokens/store/model/token' +import ManageTokenPopup from 'src/pages/SmartContract/ContractInteraction/ManageTokenPopup' +import { extendedSafeTokensSelector } from 'src/utils/safeUtils/selector' import styled from 'styled-components' -import Paragraph from '../layout/Paragraph' +import Plus from '../../assets/icons/Plus.svg' +import { OutlinedButton } from '../Button' import Field from './Field' import FundForm, { IFund } from './FundForm' import { makeSchemaInput } from './utils' -import { OutlinedButton } from '../Button' -import Plus from '../../assets/icons/Plus.svg' const Wrap = styled.div` margin-top: 32px; @@ -48,8 +49,6 @@ const Title = styled.div` margin-top: 16px; ` -let rowId = 0 - function JsonschemaForm({ schema, formData, @@ -62,8 +61,21 @@ function JsonschemaForm({ setFunds, setInvalidAmount, }): ReactElement { - const dispatch = useDispatch() - const [selectedTokens, setSelectedTokens] = useState([]) + const tokenList = useSelector(extendedSafeTokensSelector) as unknown as Token[] + const defListTokens = tokenList.map((token) => ({ + id: token.denom, + denom: token.denom, + amount: '', + tokenDecimal: token.decimals, + logoUri: token.logoUri, + type: token.type, + symbol: token.symbol, + name: token.name, + balance: token.balance.tokenBalance, + enabled: false, + })) + const [manageTokenPopupOpen, setManageTokenPopupOpen] = useState(false) + const [listTokens, setListTokens] = useState(defListTokens ?? []) const jsValidator = new Validator() if (!schema) return <> jsValidator.addSchema(schema) @@ -78,35 +90,23 @@ function JsonschemaForm({ } const handleAddFund = () => { - const newFund = { id: rowId, denom: '', amount: '' } - const newFunds = [...funds, newFund] - setFunds(newFunds) - rowId++ + setManageTokenPopupOpen(true) } - const handleDeleteFund = (id: number) => { - const updatedFunds = funds.filter((fund) => fund.id !== id) + const handleDeleteFund = (id: string) => { + const updatedFunds = funds.filter((fund: IFund) => fund.id !== id) setFunds(updatedFunds) - const updatedSelectedTokens = selectedTokens.filter((token) => token !== getDenomById(id)) - setSelectedTokens(updatedSelectedTokens) - } - - const getDenomById = (id: number) => { - const fund = funds.find((fund: IFund) => fund.id === id) - return fund ? fund.denom : '' - } - - const handleSelectToken = (denom: string) => { - const updatedSelectedTokens = [...selectedTokens] - updatedSelectedTokens.push(denom) - setSelectedTokens(updatedSelectedTokens) - } - - const handleDeselectToken = (denom: string, preToken: string) => { - const tokens = selectedTokens.filter((e) => e !== preToken) - const updatedSelectedTokens = [...tokens] - updatedSelectedTokens.push(denom) - setSelectedTokens(updatedSelectedTokens) + const updatedListTokens = listTokens.map((token) => { + if (token.id === id) { + return { + ...token, + enabled: false, + } + } + return token + }) + localStorage.setItem('listFunds', JSON.stringify(updatedListTokens)) + setListTokens(updatedListTokens) } const handleChangeAmount = (isError: boolean) => { @@ -169,14 +169,7 @@ function JsonschemaForm({ Transaction funds {funds.map((fund: IFund) => (
- +
))} @@ -184,6 +177,14 @@ function JsonschemaForm({ Add funds + setManageTokenPopupOpen(false)} + setFunds={setFunds} + listTokens={listTokens} + setListTokens={setListTokens} + defListTokens={defListTokens} + /> ) } diff --git a/src/components/TxComponents/Amount.tsx b/src/components/TxComponents/Amount.tsx index 6412990ecf..0c33ed16df 100644 --- a/src/components/TxComponents/Amount.tsx +++ b/src/components/TxComponents/Amount.tsx @@ -37,29 +37,21 @@ export default function Amount({

{label}

{listTokens ? ( - nativeToken ? ( - <> -
- {!hideLogo && {'nativeCurrencyLogoUri'}} + <> +
+ {!hideLogo && {'nativeCurrencyLogoUri'}} + {nativeToken ? (

{new BigNumber(nativeFee).plus(new BigNumber(+nativeToken.amount)).toString()} {nativeToken.symbol}

-
- {otherToken?.map((token: IFund) => ( - - ))} - - ) : ( - <> -
- {!hideLogo && {'nativeCurrencyLogoUri'}} + ) : (

{amount}

-
- {listTokens.map((token: IFund) => ( - - ))} - - ) + )} +
+ {otherToken?.map((token: IFund) => ( + + ))} + ) : (
{!hideLogo && ( diff --git a/src/pages/SmartContract/ContractInteraction/Contract.tsx b/src/pages/SmartContract/ContractInteraction/Contract.tsx index 549a58bc0a..a5ef267255 100644 --- a/src/pages/SmartContract/ContractInteraction/Contract.tsx +++ b/src/pages/SmartContract/ContractInteraction/Contract.tsx @@ -1,7 +1,11 @@ +import { Validator } from 'jsonschema' import { ReactElement, useEffect, useState } from 'react' import { useDispatch } from 'react-redux' import { FilledButton } from 'src/components/Button' import JsonschemaForm from 'src/components/JsonschemaForm' +import { IFund } from 'src/components/JsonschemaForm/FundForm' +import { makeSchemaInput } from 'src/components/JsonschemaForm/utils' +import Loader from 'src/components/Loader' import { enhanceSnackbarForAction } from 'src/logic/notifications' import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar' import { MsgTypeUrl } from 'src/logic/providers/constants/constant' @@ -9,11 +13,6 @@ import { extractPrefixedSafeAddress, extractSafeAddress } from 'src/routes/route import { simulate } from 'src/services' import styled from 'styled-components' import ReviewPopup from './ReviewPopup' -import { Validator } from 'jsonschema' -import { makeSchemaInput } from 'src/components/JsonschemaForm/utils' -import Loader from 'src/components/Loader' -import { IFund } from 'src/components/JsonschemaForm/FundForm' -import { convertAmount } from 'src/utils' const Wrap = styled.div` .preview-button { @@ -111,6 +110,12 @@ function Contract({ contractData }): ReactElement { } }, [contractData.contractAddress, contractData.executeMsgSchema]) + useEffect(() => { + return () => { + localStorage.removeItem('listFunds') + } + }, []) + if (!contractData?.executeMsgSchema || !contractData.contractAddress) return <> return ( diff --git a/src/pages/SmartContract/ContractInteraction/ManageTokenPopup.tsx b/src/pages/SmartContract/ContractInteraction/ManageTokenPopup.tsx new file mode 100644 index 0000000000..52af06eaea --- /dev/null +++ b/src/pages/SmartContract/ContractInteraction/ManageTokenPopup.tsx @@ -0,0 +1,157 @@ +import { useEffect, useState } from 'react' +import { FilledButton, OutlinedNeutralButton } from 'src/components/Button' +import Checkbox from 'src/components/Input/Checkbox' +import { Popup } from 'src/components/Popup' +import Footer from 'src/components/Popup/Footer' +import Header from 'src/components/Popup/Header' +import styled from 'styled-components' + +const Wrap = styled.div` + width: 480px; + > div { + padding: 24px; + } + > div:first-child { + border-bottom: 1px solid #404047; + } + .token-list { + margin-top: 18px; + .title { + font-weight: 600; + font-size: 16px; + line-height: 20px; + } + .list { + margin-top: 16px; + border-radius: 8px; + padding: 0px 16px; + background: #363843; + max-height: 60vh; + overflow: auto; + } + } +` + +const Col = styled.div` + display: flex; + align-items: center; +` + +const Row = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + padding: 0px 16px; +` + +const CoinWrapper = styled.div` + margin: 8px 0px; + min-height: 40px; + display: flex; + justify-content: space-between; + align-items: center; + > div { + text-transform: uppercase; + } +` + +const Title = styled.div` + font-weight: 600; + font-size: 16px; + line-height: 20px; + margin-left: 16px; +` + +const List = styled.div` + margin-top: 16px; + border-radius: 8px; + padding: 0px 16px; + background: #363843; + max-height: 60vh; + overflow: auto; +` +const WrapToken = styled.div` + margin-left: 16px; +` + +const CoinConfig = ({ token, isEnable, setToggle }) => { + return ( + + + setToggle()} /> + {token.name} + + +
{token.balance}
+ +
+ ) +} + +const ManageTokenPopup = ({ open, onClose, setFunds, listTokens, setListTokens, defListTokens }) => { + const [toggleAll, setToggleAll] = useState(false) + + const toggleAllHandler = () => { + setListTokens(listTokens?.map((token) => ({ ...token, enabled: !toggleAll }))) + setToggleAll(!toggleAll) + } + + const handleAddFunds = () => { + const listFunds = listTokens.filter((token) => token.enabled) + localStorage.setItem('listFunds', JSON.stringify(listTokens)) + setFunds(listFunds) + onClose() + } + + const handleClose = () => { + const storedListTokens = localStorage.getItem('listFunds') + setListTokens(storedListTokens ? JSON.parse(storedListTokens) : defListTokens) + onClose() + } + + useEffect(() => { + const isSelectAll = listTokens?.every((token) => token?.enabled) + setToggleAll(isSelectAll) + }, [listTokens]) + + return ( + +
+ +
+ + + + Asset + + + Balance + + + + {listTokens?.map((token, i) => { + return ( + + setListTokens( + listTokens.map((token, id) => (i === id ? { ...token, enabled: !token.enabled } : token)), + ) + } + /> + ) + })} + +
+
+ Cancel + Apply +
+
+ + ) +} + +export default ManageTokenPopup From df46870cf096109bd6f51a1ccae3e4b500f4cbf0 Mon Sep 17 00:00:00 2001 From: HoangNDM6 Date: Mon, 5 Jun 2023 23:50:21 +0700 Subject: [PATCH 33/69] handle funds transaction --- src/components/JsonschemaForm/index.tsx | 26 +++++-------------- src/logic/contracts/store/actions/index.ts | 4 +++ src/logic/contracts/store/reducer/index.ts | 26 +++++++++++++++++++ src/logic/contracts/store/selectors/index.ts | 6 +++++ src/logic/safe/store/index.ts | 4 +++ .../ContractInteraction/Contract.tsx | 21 +++++++++++++-- .../ContractInteraction/ManageTokenPopup.tsx | 16 +++++++++--- 7 files changed, 79 insertions(+), 24 deletions(-) create mode 100644 src/logic/contracts/store/actions/index.ts create mode 100644 src/logic/contracts/store/reducer/index.ts create mode 100644 src/logic/contracts/store/selectors/index.ts diff --git a/src/components/JsonschemaForm/index.tsx b/src/components/JsonschemaForm/index.tsx index 4208c117cd..60ed360ce6 100644 --- a/src/components/JsonschemaForm/index.tsx +++ b/src/components/JsonschemaForm/index.tsx @@ -1,9 +1,8 @@ import { Validator } from 'jsonschema' -import { ReactElement, useEffect, useState } from 'react' -import { useSelector } from 'react-redux' -import { Token } from 'src/logic/tokens/store/model/token' +import { ReactElement, useState } from 'react' +import { useDispatch } from 'react-redux' +import { addToFunds } from 'src/logic/contracts/store/actions' import ManageTokenPopup from 'src/pages/SmartContract/ContractInteraction/ManageTokenPopup' -import { extendedSafeTokensSelector } from 'src/utils/safeUtils/selector' import styled from 'styled-components' import Plus from '../../assets/icons/Plus.svg' import { OutlinedButton } from '../Button' @@ -60,22 +59,11 @@ function JsonschemaForm({ funds, setFunds, setInvalidAmount, + defListTokens, }): ReactElement { - const tokenList = useSelector(extendedSafeTokensSelector) as unknown as Token[] - const defListTokens = tokenList.map((token) => ({ - id: token.denom, - denom: token.denom, - amount: '', - tokenDecimal: token.decimals, - logoUri: token.logoUri, - type: token.type, - symbol: token.symbol, - name: token.name, - balance: token.balance.tokenBalance, - enabled: false, - })) + const dispatch = useDispatch() const [manageTokenPopupOpen, setManageTokenPopupOpen] = useState(false) - const [listTokens, setListTokens] = useState(defListTokens ?? []) + const [listTokens, setListTokens] = useState(defListTokens ?? []) const jsValidator = new Validator() if (!schema) return <> jsValidator.addSchema(schema) @@ -105,7 +93,7 @@ function JsonschemaForm({ } return token }) - localStorage.setItem('listFunds', JSON.stringify(updatedListTokens)) + dispatch(addToFunds(updatedListTokens)) setListTokens(updatedListTokens) } diff --git a/src/logic/contracts/store/actions/index.ts b/src/logic/contracts/store/actions/index.ts new file mode 100644 index 0000000000..f46561e1a0 --- /dev/null +++ b/src/logic/contracts/store/actions/index.ts @@ -0,0 +1,4 @@ +import { createAction } from 'redux-actions' + +export const ADD_TO_FUNDS = 'ADD_TO_FUNDS' +export const addToFunds = createAction(ADD_TO_FUNDS) diff --git a/src/logic/contracts/store/reducer/index.ts b/src/logic/contracts/store/reducer/index.ts new file mode 100644 index 0000000000..d6769f44c6 --- /dev/null +++ b/src/logic/contracts/store/reducer/index.ts @@ -0,0 +1,26 @@ +import { Action, handleActions } from 'redux-actions' +import { IFund } from 'src/components/JsonschemaForm/FundForm' +import { ADD_TO_FUNDS } from '../actions' + +export const FUNDS_REDUCER_ID = 'funds' + +export type FundStateType = { + funds: IFund[] +} + +const initialState = { + funds: [], +} + +type FundPayloadType = FundStateType | IFund[] + +const delegationReducer = handleActions( + { + [ADD_TO_FUNDS]: (state, action: Action) => ({ + funds: action.payload, + }), + }, + initialState, +) + +export default delegationReducer diff --git a/src/logic/contracts/store/selectors/index.ts b/src/logic/contracts/store/selectors/index.ts new file mode 100644 index 0000000000..1e983d0d72 --- /dev/null +++ b/src/logic/contracts/store/selectors/index.ts @@ -0,0 +1,6 @@ +import { createSelector } from 'reselect' +import { FUNDS_REDUCER_ID } from '../reducer' + +const fundsSelector = (state) => state[FUNDS_REDUCER_ID] + +export const getFunds = createSelector(fundsSelector, (state) => state.funds) diff --git a/src/logic/safe/store/index.ts b/src/logic/safe/store/index.ts index 1f4e75086e..6814dfbc85 100644 --- a/src/logic/safe/store/index.ts +++ b/src/logic/safe/store/index.ts @@ -56,6 +56,8 @@ import { SafeReducerMap } from 'src/logic/safe/store/reducer/types/safe' import validatorReducer, { VALIDATOR_REDUCER_ID } from 'src/logic/validator/store/reducer' import { IProposal } from 'src/types/proposal' import { LS_NAMESPACE, LS_SEPARATOR } from 'src/utils/constants' +import fundsReducer, { FUNDS_REDUCER_ID } from 'src/logic/contracts/store/reducer' +import { IFund } from 'src/components/JsonschemaForm/FundForm' const CURRENCY_KEY = `${CURRENCY_REDUCER_ID}.selectedCurrency` @@ -107,6 +109,7 @@ const reducers = { [PROPOSALS_REDUCER_ID]: proposalsReducer, [VALIDATOR_REDUCER_ID]: validatorReducer, [DELEGATION_REDUCER_ID]: delegationReducer, + [FUNDS_REDUCER_ID]: fundsReducer, } const rootReducer = combineReducers(reducers) @@ -133,6 +136,7 @@ export type AppReduxState = CombinedState<{ [PROPOSALS_REDUCER_ID]: IProposal[] [VALIDATOR_REDUCER_ID]: ValidatorStateType [DELEGATION_REDUCER_ID]: DelegationStateType + [FUNDS_REDUCER_ID]: IFund[] }> export const store: any = createStore(rootReducer, load(LS_CONFIG), enhancer) diff --git a/src/pages/SmartContract/ContractInteraction/Contract.tsx b/src/pages/SmartContract/ContractInteraction/Contract.tsx index a5ef267255..a261b32d2f 100644 --- a/src/pages/SmartContract/ContractInteraction/Contract.tsx +++ b/src/pages/SmartContract/ContractInteraction/Contract.tsx @@ -1,6 +1,6 @@ import { Validator } from 'jsonschema' import { ReactElement, useEffect, useState } from 'react' -import { useDispatch } from 'react-redux' +import { useDispatch, useSelector } from 'react-redux' import { FilledButton } from 'src/components/Button' import JsonschemaForm from 'src/components/JsonschemaForm' import { IFund } from 'src/components/JsonschemaForm/FundForm' @@ -13,6 +13,9 @@ import { extractPrefixedSafeAddress, extractSafeAddress } from 'src/routes/route import { simulate } from 'src/services' import styled from 'styled-components' import ReviewPopup from './ReviewPopup' +import { addToFunds } from 'src/logic/contracts/store/actions' +import { extendedSafeTokensSelector } from 'src/utils/safeUtils/selector' +import { Token } from 'src/logic/tokens/store/model/token' const Wrap = styled.div` .preview-button { @@ -38,6 +41,19 @@ function Contract({ contractData }): ReactElement { const [schema, setSchema] = useState() const [loading, setLoading] = useState(false) const [invalidAmount, setInvalidAmount] = useState(false) + const tokenList = useSelector(extendedSafeTokensSelector) as unknown as Token[] + const defListTokens = tokenList.map((token) => ({ + id: token.denom, + denom: token.denom, + amount: '', + tokenDecimal: token.decimals, + logoUri: token.logoUri, + type: token.type, + symbol: token.symbol, + name: token.name, + balance: token.balance.tokenBalance, + enabled: false, + })) as IFund[] const preview = async () => { try { setLoading(true) @@ -112,7 +128,7 @@ function Contract({ contractData }): ReactElement { useEffect(() => { return () => { - localStorage.removeItem('listFunds') + dispatch(addToFunds(defListTokens)) } }, []) @@ -131,6 +147,7 @@ function Contract({ contractData }): ReactElement { funds={funds} setFunds={setFunds} setInvalidAmount={setInvalidAmount} + defListTokens={defListTokens} />
diff --git a/src/pages/SmartContract/ContractInteraction/ManageTokenPopup.tsx b/src/pages/SmartContract/ContractInteraction/ManageTokenPopup.tsx index 52af06eaea..f9e1b2d495 100644 --- a/src/pages/SmartContract/ContractInteraction/ManageTokenPopup.tsx +++ b/src/pages/SmartContract/ContractInteraction/ManageTokenPopup.tsx @@ -1,9 +1,13 @@ import { useEffect, useState } from 'react' +import { useDispatch, useSelector } from 'react-redux' import { FilledButton, OutlinedNeutralButton } from 'src/components/Button' import Checkbox from 'src/components/Input/Checkbox' import { Popup } from 'src/components/Popup' import Footer from 'src/components/Popup/Footer' import Header from 'src/components/Popup/Header' +import { addToFunds } from 'src/logic/contracts/store/actions' +import { getFunds } from 'src/logic/contracts/store/selectors' + import styled from 'styled-components' const Wrap = styled.div` @@ -90,6 +94,8 @@ const CoinConfig = ({ token, isEnable, setToggle }) => { const ManageTokenPopup = ({ open, onClose, setFunds, listTokens, setListTokens, defListTokens }) => { const [toggleAll, setToggleAll] = useState(false) + const funds = useSelector(getFunds) + const dispatch = useDispatch() const toggleAllHandler = () => { setListTokens(listTokens?.map((token) => ({ ...token, enabled: !toggleAll }))) @@ -98,14 +104,18 @@ const ManageTokenPopup = ({ open, onClose, setFunds, listTokens, setListTokens, const handleAddFunds = () => { const listFunds = listTokens.filter((token) => token.enabled) - localStorage.setItem('listFunds', JSON.stringify(listTokens)) + dispatch(addToFunds(listTokens)) setFunds(listFunds) onClose() } const handleClose = () => { - const storedListTokens = localStorage.getItem('listFunds') - setListTokens(storedListTokens ? JSON.parse(storedListTokens) : defListTokens) + if (!funds || funds.length == 0) { + dispatch(addToFunds(defListTokens)) + setListTokens(defListTokens) + } else { + setListTokens(funds) + } onClose() } From 6460c1923b814c386aa8e84e47a6c1f21daad188 Mon Sep 17 00:00:00 2001 From: HoangNDM6 Date: Tue, 6 Jun 2023 11:07:50 +0700 Subject: [PATCH 34/69] update UI --- src/components/Input/Search/index.tsx | 10 +- src/components/JsonschemaForm/FundForm.tsx | 1 + src/components/JsonschemaForm/index.tsx | 6 +- .../ContractInteraction/Contract.tsx | 2 + .../ContractInteraction/ManageTokenPopup.tsx | 114 +++++++++++------- 5 files changed, 83 insertions(+), 50 deletions(-) diff --git a/src/components/Input/Search/index.tsx b/src/components/Input/Search/index.tsx index 08517f4145..ecf6543800 100644 --- a/src/components/Input/Search/index.tsx +++ b/src/components/Input/Search/index.tsx @@ -1,3 +1,4 @@ +import { ChangeEvent } from 'react' import SearchIcon from 'src/assets/icons/search.svg' import styled from 'styled-components' @@ -19,10 +20,15 @@ const Wrap = styled.div` width: 100%; } ` -export default function SearchInput({ placeholder }) { +interface ISearchInputProps { + placeholder: string + onChange?: (event: ChangeEvent) => void +} + +export default function SearchInput({ placeholder, onChange }: ISearchInputProps) { return ( - + ) diff --git a/src/components/JsonschemaForm/FundForm.tsx b/src/components/JsonschemaForm/FundForm.tsx index ce2c54997a..7be9a0bc53 100644 --- a/src/components/JsonschemaForm/FundForm.tsx +++ b/src/components/JsonschemaForm/FundForm.tsx @@ -52,6 +52,7 @@ export type IFund = { symbol: string name: string balance: string + address: string enabled?: boolean } diff --git a/src/components/JsonschemaForm/index.tsx b/src/components/JsonschemaForm/index.tsx index 60ed360ce6..7b052bcb46 100644 --- a/src/components/JsonschemaForm/index.tsx +++ b/src/components/JsonschemaForm/index.tsx @@ -5,7 +5,7 @@ import { addToFunds } from 'src/logic/contracts/store/actions' import ManageTokenPopup from 'src/pages/SmartContract/ContractInteraction/ManageTokenPopup' import styled from 'styled-components' import Plus from '../../assets/icons/Plus.svg' -import { OutlinedButton } from '../Button' +import { OutlinedNeutralButton } from '../Button' import Field from './Field' import FundForm, { IFund } from './FundForm' import { makeSchemaInput } from './utils' @@ -160,10 +160,10 @@ function JsonschemaForm({
))} - + Add funds - +
{ try { setLoading(true) diff --git a/src/pages/SmartContract/ContractInteraction/ManageTokenPopup.tsx b/src/pages/SmartContract/ContractInteraction/ManageTokenPopup.tsx index f9e1b2d495..0290d2e084 100644 --- a/src/pages/SmartContract/ContractInteraction/ManageTokenPopup.tsx +++ b/src/pages/SmartContract/ContractInteraction/ManageTokenPopup.tsx @@ -1,7 +1,9 @@ -import { useEffect, useState } from 'react' +import { ChangeEvent, useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import { FilledButton, OutlinedNeutralButton } from 'src/components/Button' import Checkbox from 'src/components/Input/Checkbox' +import SearchInput from 'src/components/Input/Search' +import { IFund } from 'src/components/JsonschemaForm/FundForm' import { Popup } from 'src/components/Popup' import Footer from 'src/components/Popup/Footer' import Header from 'src/components/Popup/Header' @@ -18,21 +20,8 @@ const Wrap = styled.div` > div:first-child { border-bottom: 1px solid #404047; } - .token-list { - margin-top: 18px; - .title { - font-weight: 600; - font-size: 16px; - line-height: 20px; - } - .list { - margin-top: 16px; - border-radius: 8px; - padding: 0px 16px; - background: #363843; - max-height: 60vh; - overflow: auto; - } + .search-input { + margin-bottom: 18px; } ` @@ -71,10 +60,12 @@ const List = styled.div` border-radius: 8px; padding: 0px 16px; background: #363843; - max-height: 60vh; + max-height: 50vh; overflow: auto; ` const WrapToken = styled.div` + display: flex; + align-items: center; margin-left: 16px; ` @@ -83,7 +74,14 @@ const CoinConfig = ({ token, isEnable, setToggle }) => { setToggle()} /> - {token.name} + + {token.name} + {token.symbol} +
{token.balance}
@@ -94,9 +92,14 @@ const CoinConfig = ({ token, isEnable, setToggle }) => { const ManageTokenPopup = ({ open, onClose, setFunds, listTokens, setListTokens, defListTokens }) => { const [toggleAll, setToggleAll] = useState(false) + const [filteredTokens, setFilteredTokens] = useState(listTokens) const funds = useSelector(getFunds) const dispatch = useDispatch() + useEffect(() => { + setFilteredTokens(listTokens) + }, [listTokens]) + const toggleAllHandler = () => { setListTokens(listTokens?.map((token) => ({ ...token, enabled: !toggleAll }))) setToggleAll(!toggleAll) @@ -119,6 +122,19 @@ const ManageTokenPopup = ({ open, onClose, setFunds, listTokens, setListTokens, onClose() } + const handleSearch = (event: ChangeEvent) => { + const searchTerm = event.target.value.toLowerCase() + const filteredTokens = listTokens.filter((token: IFund) => { + const { symbol, name, address } = token + return ( + symbol.toLowerCase().includes(searchTerm) || + name.toLowerCase().includes(searchTerm) || + address.toLowerCase().includes(searchTerm) + ) + }) + setFilteredTokens(filteredTokens) + } + useEffect(() => { const isSelectAll = listTokens?.every((token) => token?.enabled) setToggleAll(isSelectAll) @@ -128,36 +144,44 @@ const ManageTokenPopup = ({ open, onClose, setFunds, listTokens, setListTokens,
-
- - - - Asset - - - Balance - - - - {listTokens?.map((token, i) => { - return ( - - setListTokens( - listTokens.map((token, id) => (i === id ? { ...token, enabled: !token.enabled } : token)), - ) - } - /> - ) - })} - +
+ {listTokens.length > 10 && ( +
+ +
+ )} + +
+ + + + Asset + + + Balance + + + + {filteredTokens?.map((token, i) => { + return ( + + setListTokens( + listTokens.map((token, id) => (i === id ? { ...token, enabled: !token.enabled } : token)), + ) + } + /> + ) + })} + +
Cancel - Apply + Select
From 0d4bf2e460456d268581aa583b95399a363b856d Mon Sep 17 00:00:00 2001 From: HoangNDM6 Date: Tue, 6 Jun 2023 15:02:51 +0700 Subject: [PATCH 35/69] fix list default token --- src/pages/Assets/Tokens/ManageTokenPopup.tsx | 28 ++++++++++++++++---- src/services/index.ts | 18 +------------ 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/src/pages/Assets/Tokens/ManageTokenPopup.tsx b/src/pages/Assets/Tokens/ManageTokenPopup.tsx index 465ad53bd1..08e0e2eebc 100644 --- a/src/pages/Assets/Tokens/ManageTokenPopup.tsx +++ b/src/pages/Assets/Tokens/ManageTokenPopup.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react' +import { ChangeEvent, useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import { FilledButton } from 'src/components/Button' import Checkbox from 'src/components/Input/Checkbox' @@ -41,7 +41,7 @@ const Row = styled.div` ` export default function ManageTokenPopup({ open, onClose }) { const dispatch = useDispatch() - const [toggleAll, setToggleAll] = useState(false) + const [toggleAll, setToggleAll] = useState(false) const { coinConfig, address } = useSelector(currentSafeWithNames) const [config, setConfig] = useState(coinConfig) const applyHandler = () => { @@ -63,12 +63,29 @@ export default function ManageTokenPopup({ open, onClose }) { setToggleAll(!toggleAll) } + const handleSearch = (event: ChangeEvent) => { + const searchTerm = event.target.value.toLowerCase() + const filteredTokens = coinConfig?.filter((token) => { + return ( + token?.symbol?.toLowerCase().includes(searchTerm) || + token?.name?.toLowerCase().includes(searchTerm) || + token?.address?.toLowerCase().includes(searchTerm) + ) + }) + setConfig(filteredTokens) + } + + useEffect(() => { + const isSelectAll = config?.every((token) => token?.enable) + setToggleAll(!!isSelectAll) + }, [config]) + return (
- +
Token list
@@ -81,6 +98,7 @@ export default function ManageTokenPopup({ open, onClose }) { return ( setConfig(config.map((cf, id) => (i == id ? { ...cf, enable: !cf.enable } : cf)))} @@ -108,11 +126,11 @@ const CoinWrapper = styled.div` text-transform: uppercase; } ` -const CoinConfig = ({ name, isEnable, setToggle }) => { +const CoinConfig = ({ name, isEnable, setToggle, type }) => { return (
{name}
- setToggle()} /> + {type !== 'native' ? setToggle()} /> : <>}
) } diff --git a/src/services/index.ts b/src/services/index.ts index 1d677f6eb5..997660bdb0 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -126,23 +126,7 @@ export function getMChainsConfig(): Promise { // 'SAFE_TX_GAS_OPTIONAL', // 'SPENDING_LIMIT', ], - coinConfig: e?.coinConfig || [ - { - name: 'AURA', - }, - { - name: 'BTC', - }, - { - name: 'ETH', - }, - { - name: 'BNB', - }, - { - name: 'USDT', - }, - ], + coinConfig: e?.coinConfig || [], } }, ) From 9355cda39b3e62256d68eb555af805d4aca8b634 Mon Sep 17 00:00:00 2001 From: imHson Date: Wed, 7 Jun 2023 15:53:00 +0700 Subject: [PATCH 36/69] fallback undefined token registry --- .vscode/settings.json | 4 +-- .../tokens/store/actions/fetchSafeTokens.ts | 28 +++++++++---------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 1015ebd79f..2534f0758f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,4 @@ { - "typescript.tsdk": "node_modules\\typescript\\lib", - "editor.formatOnSave": true + "typescript.tsdk": "node_modules\\typescript\\lib", + "editor.formatOnSave": true } \ No newline at end of file diff --git a/src/logic/tokens/store/actions/fetchSafeTokens.ts b/src/logic/tokens/store/actions/fetchSafeTokens.ts index b6c959fe9d..a9975b0b74 100644 --- a/src/logic/tokens/store/actions/fetchSafeTokens.ts +++ b/src/logic/tokens/store/actions/fetchSafeTokens.ts @@ -128,7 +128,7 @@ export const fetchMSafeTokens = .filter((balance) => balance.denom != chainInfo.denom) .forEach((data: any) => { const tokenDetail = tokenDetailsList['ibc'].find((token) => token.cosmosDenom == data.minimal_denom) - if (coinConfig.findIndex((config) => config.address == tokenDetail.address) == -1) { + if (tokenDetail && coinConfig.findIndex((config) => config.address == tokenDetail.address) == -1) { coinConfig.push({ name: tokenDetail.name, address: tokenDetail.address, @@ -138,16 +138,16 @@ export const fetchMSafeTokens = }) } balances.push({ - tokenBalance: `${humanReadableValue(+data?.amount > 0 ? data?.amount : 0, tokenDetail.decimals)}`, - tokenAddress: tokenDetail.address, - decimals: tokenDetail.decimals, + tokenBalance: `${humanReadableValue(+data?.amount > 0 ? data?.amount : 0, tokenDetail?.decimals || 6)}`, + tokenAddress: tokenDetail?.address, + decimals: tokenDetail?.decimals || 6, logoUri: tokenDetail?.icon ? `https://aura-nw.github.io/token-registry/images/${tokenDetail?.icon}` : 'https://aura-nw.github.io/token-registry/images/undefined.png', - name: tokenDetail.name, - symbol: tokenDetail.coinDenom, - denom: tokenDetail.minCoinDenom, - cosmosDenom: tokenDetail.cosmosDenom, + name: tokenDetail?.name, + symbol: tokenDetail?.coinDenom, + denom: tokenDetail?.minCoinDenom, + cosmosDenom: tokenDetail?.cosmosDenom, type: 'ibc', }) }) @@ -155,7 +155,7 @@ export const fetchMSafeTokens = if (safeInfo.assets.CW20.asset.length > 0) { safeInfo.assets.CW20.asset.forEach((data) => { const tokenDetail = tokenDetailsList['cw20'].find((token) => token.address == data.contract_address) - if (coinConfig.findIndex((config) => config.address == tokenDetail.address) == -1) { + if (tokenDetail && coinConfig.findIndex((config) => config.address == tokenDetail.address) == -1) { coinConfig.push({ name: tokenDetail.name, address: tokenDetail.address, @@ -165,15 +165,15 @@ export const fetchMSafeTokens = }) } balances.push({ - tokenBalance: `${humanReadableValue(+data?.balance > 0 ? data?.balance : 0, tokenDetail.decimals)}`, - tokenAddress: tokenDetail.address, - decimals: tokenDetail.decimals, + tokenBalance: `${humanReadableValue(+data?.balance > 0 ? data?.balance : 0, tokenDetail?.decimals || 6)}`, + tokenAddress: tokenDetail?.address, + decimals: tokenDetail?.decimals || 6, name: tokenDetail?.name, logoUri: tokenDetail?.icon ? `https://aura-nw.github.io/token-registry/images/${tokenDetail?.icon}` : 'https://aura-nw.github.io/token-registry/images/undefined.png', - symbol: tokenDetail.symbol, - denom: tokenDetail.symbol, + symbol: tokenDetail?.symbol, + denom: tokenDetail?.symbol, type: 'CW20', }) }) From 06bb8b8d6493d1ddb35ea338d4e1f4b94db76ff3 Mon Sep 17 00:00:00 2001 From: HoangNDM6 Date: Mon, 12 Jun 2023 10:40:14 +0700 Subject: [PATCH 37/69] handle manage token --- src/assets/icons/ic_close.svg | 3 + src/assets/icons/ic_empty.svg | 19 ++ src/components/Input/TextField/index.tsx | 16 +- src/components/Popup/index.tsx | 13 +- .../tokens/store/actions/fetchSafeTokens.ts | 57 +++--- src/pages/Assets/Tokens/ImportTokenPopup.tsx | 162 ++++++++++++++++++ src/pages/Assets/Tokens/ManageTokenPopup.tsx | 157 +++++++++++++---- src/pages/Assets/Tokens/index.tsx | 156 +++++++++++------ .../ContractInteraction/ManageTokenPopup.tsx | 2 +- src/pages/Voting/ReviewTxPopup.tsx | 2 +- src/services/index.ts | 13 ++ 11 files changed, 483 insertions(+), 117 deletions(-) create mode 100644 src/assets/icons/ic_close.svg create mode 100644 src/assets/icons/ic_empty.svg create mode 100644 src/pages/Assets/Tokens/ImportTokenPopup.tsx diff --git a/src/assets/icons/ic_close.svg b/src/assets/icons/ic_close.svg new file mode 100644 index 0000000000..f15dfdbc43 --- /dev/null +++ b/src/assets/icons/ic_close.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/ic_empty.svg b/src/assets/icons/ic_empty.svg new file mode 100644 index 0000000000..72c14befd1 --- /dev/null +++ b/src/assets/icons/ic_empty.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/components/Input/TextField/index.tsx b/src/components/Input/TextField/index.tsx index d1de3dce6d..4b7aae97c1 100644 --- a/src/components/Input/TextField/index.tsx +++ b/src/components/Input/TextField/index.tsx @@ -52,6 +52,13 @@ const Wrap = styled.div` font-size: 12px; margin-top: 6px; } + .disabled-input { + background: #494c58; + border: 1px solid #494c58; + input { + background: #494c58; + } + } ` export default function TextField({ label, @@ -64,10 +71,11 @@ export default function TextField({ required, errorMsg, min, + disabled, }: { label: string value: any - onChange: (value: string) => void + onChange?: (value: string) => void type?: React.HTMLInputTypeAttribute autoFocus?: boolean endIcon?: any @@ -75,6 +83,7 @@ export default function TextField({ required?: boolean errorMsg?: string min?: number + disabled?: boolean }) { const [isFocus, setIsFocus] = useState(false) return ( @@ -85,16 +94,17 @@ export default function TextField({ {required && *}
)} -
+
onChange(e.target.value)} + onChange={(e) => onChange && onChange(e.target.value)} onFocus={() => setIsFocus(true)} onBlur={() => setIsFocus(false)} min={min} + disabled={disabled} /> {endIcon &&
{endIcon}
}
diff --git a/src/components/Popup/index.tsx b/src/components/Popup/index.tsx index 33fbf069d5..a3795a37f7 100644 --- a/src/components/Popup/index.tsx +++ b/src/components/Popup/index.tsx @@ -1,6 +1,5 @@ import { Modal } from '@material-ui/core' import { ReactElement, ReactNode } from 'react' -import { bgBox } from 'src/theme/variables' import styled from 'styled-components' const PopupWrapper = styled(Modal)` @@ -34,9 +33,18 @@ interface PopupProps { open: boolean paperClassName?: string title?: string + keepMounted?: boolean } -const Popup = ({ children, description, handleClose, open, paperClassName, title }: PopupProps): ReactElement => { +const Popup = ({ + children, + description, + handleClose, + open, + paperClassName, + title, + keepMounted = false, +}: PopupProps): ReactElement => { return (
{children}
diff --git a/src/logic/tokens/store/actions/fetchSafeTokens.ts b/src/logic/tokens/store/actions/fetchSafeTokens.ts index a9975b0b74..d321035b66 100644 --- a/src/logic/tokens/store/actions/fetchSafeTokens.ts +++ b/src/logic/tokens/store/actions/fetchSafeTokens.ts @@ -95,22 +95,35 @@ export const fetchSafeTokens = export const fetchMSafeTokens = (safeInfo: IMSafeInfo) => async (dispatch: Dispatch, getState: () => AppReduxState): Promise => { + let listTokens: any[] = [] + const cw20Tokens = safeInfo.assets.CW20.asset.map((asset) => ({ + name: asset.asset_info.data.name, + decimals: asset.asset_info.data.decimals, + symbol: asset.asset_info.data.symbol, + address: asset.contract_address, + _id: asset['_id'], + })) + const listSafeTokens = [...safeInfo.balance, ...cw20Tokens] const state = getState() const safe = safeByAddressSelector(state, safeInfo.address) if (safeInfo?.balance) { - const coinConfig = safe?.coinConfig?.length - ? safe?.coinConfig - : getCoinConfig().map((config) => { - return { ...config, enable: true } - }) const listChain = getChains() const tokenDetailsListData = await getTokenDetail() const tokenDetailsList = await tokenDetailsListData.json() + listTokens = [...tokenDetailsList['ibc'], ...tokenDetailsList['cw20']] + const filteredListTokens = listTokens.map((token) => { + const isExist = listSafeTokens.some((t) => t.denom === token.minCoinDenom || t.address === token.address) + if (isExist) { + return { ...token, enable: true } + } else { + return { ...token, enable: false } + } + }) const chainInfo: any = listChain.find((x: any) => x.internalChainId === safeInfo?.internalChainId) const nativeTokenData = safeInfo.balance.find((balance) => balance.denom == chainInfo.denom) const balances: any[] = [] if (nativeTokenData) { - balances.push({ + const nativeToken = { tokenBalance: `${humanReadableValue( +nativeTokenData?.amount > 0 ? nativeTokenData?.amount : 0, chainInfo.nativeCurrency.decimals, @@ -122,21 +135,24 @@ export const fetchMSafeTokens = symbol: chainInfo.nativeCurrency.symbol, denom: chainInfo.denom, type: 'native', - }) + } + balances.push(nativeToken) + filteredListTokens.unshift(nativeToken) } + + const coinConfig = safe?.coinConfig?.length + ? filteredListTokens + .filter( + (item) => + !safe?.coinConfig?.some((token) => token.denom === item.denom || token.address === item.address), + ) + .concat(safe?.coinConfig) + : filteredListTokens + safeInfo.balance .filter((balance) => balance.denom != chainInfo.denom) .forEach((data: any) => { const tokenDetail = tokenDetailsList['ibc'].find((token) => token.cosmosDenom == data.minimal_denom) - if (tokenDetail && coinConfig.findIndex((config) => config.address == tokenDetail.address) == -1) { - coinConfig.push({ - name: tokenDetail.name, - address: tokenDetail.address, - denom: tokenDetail.minCoinDenom, - type: 'ibc', - enable: false, - }) - } balances.push({ tokenBalance: `${humanReadableValue(+data?.amount > 0 ? data?.amount : 0, tokenDetail?.decimals || 6)}`, tokenAddress: tokenDetail?.address, @@ -155,15 +171,6 @@ export const fetchMSafeTokens = if (safeInfo.assets.CW20.asset.length > 0) { safeInfo.assets.CW20.asset.forEach((data) => { const tokenDetail = tokenDetailsList['cw20'].find((token) => token.address == data.contract_address) - if (tokenDetail && coinConfig.findIndex((config) => config.address == tokenDetail.address) == -1) { - coinConfig.push({ - name: tokenDetail.name, - address: tokenDetail.address, - denom: tokenDetail.symbol, - type: 'cw20', - enable: false, - }) - } balances.push({ tokenBalance: `${humanReadableValue(+data?.balance > 0 ? data?.balance : 0, tokenDetail?.decimals || 6)}`, tokenAddress: tokenDetail?.address, diff --git a/src/pages/Assets/Tokens/ImportTokenPopup.tsx b/src/pages/Assets/Tokens/ImportTokenPopup.tsx new file mode 100644 index 0000000000..37ed8d87b4 --- /dev/null +++ b/src/pages/Assets/Tokens/ImportTokenPopup.tsx @@ -0,0 +1,162 @@ +import { useEffect, useState } from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { FilledButton, OutlinedNeutralButton } from 'src/components/Button' +import Gap from 'src/components/Gap' +import TextField from 'src/components/Input/TextField' +import Loader from 'src/components/Loader' +import { Popup } from 'src/components/Popup' +import Header from 'src/components/Popup/Header' +import { getInternalChainId } from 'src/config' +import { updateSafe } from 'src/logic/safe/store/actions/updateSafe' +import { currentSafeWithNames } from 'src/logic/safe/store/selectors' +import { getContract, getDetailToken } from 'src/services' +import { isValidAddress } from 'src/utils/isValidAddress' +import styled from 'styled-components' + +const Wrap = styled.div` + width: 480px; + > div { + padding: 24px; + } + .err-mess { + color: #d5625e; + } +` + +export const Footer = styled.div` + padding: 24px; + border-top: 1px solid #404047; + display: flex; + justify-content: end; + > button:nth-child(1) { + margin-right: 24px; + } +` + +type IToken = { + address: string + symbol: string + name: string + isAddedToken: boolean + enable: boolean + decimals: number +} +const defaultToken = { + address: '', + symbol: '', + name: '', + enable: false, + isAddedToken: true, + decimals: 0, +} + +const ImportTokenPopup = ({ open, onBack, onClose }) => { + const dispatch = useDispatch() + const internalChainId = getInternalChainId() + const [token, setToken] = useState(defaultToken) + const { coinConfig, address } = useSelector(currentSafeWithNames) + const [isVerifiedContract, setIsVerifiedContract] = useState(null) + + const getContractDetail = async () => { + setIsVerifiedContract('loading') + const { Data } = await getContract(token.address, internalChainId) + const { data } = await getDetailToken(token.address) + if (Data) { + setIsVerifiedContract(Data.verification ? 'true' : 'false') + } else { + setIsVerifiedContract('false') + } + if (data) { + setToken({ ...token, name: data.name, symbol: data.symbol, decimals: data.decimals }) + } + } + + useEffect(() => { + setIsVerifiedContract(null) + if (!token.address) { + return + } + const isValid = isValidAddress(token.address) + if (isValid) { + getContractDetail() + } else { + setIsVerifiedContract('false') + } + }, [token.address]) + + const getContractStatus = () => { + if (isVerifiedContract === 'loading') { + return + } + } + + const handleImport = () => { + let newCoinConfig + if (coinConfig) { + newCoinConfig = [...coinConfig] + if (!newCoinConfig.some((item) => item.address === token.address)) { + newCoinConfig.push(token) + } + } + dispatch( + updateSafe({ + address, + coinConfig: newCoinConfig ?? coinConfig, + }), + ) + onClose() + setToken(defaultToken) + } + + return ( + +
{ + onClose() + setToken(defaultToken) + }} + hideNetwork={true} + /> + +
+ setToken({ ...token, address: value.trim() })} + endIcon={isVerifiedContract != null ? getContractStatus() : null} + autoFocus={true} + errorMsg={isVerifiedContract === 'false' ? 'Invalid Token contract address' : ''} + /> + + {isVerifiedContract === 'true' ? ( + <> + + + + + ) : ( + <> + )} +
+ +
+ { + onBack() + setToken(defaultToken) + }} + > + Back + + + Import + +
+
+ + ) +} + +export default ImportTokenPopup diff --git a/src/pages/Assets/Tokens/ManageTokenPopup.tsx b/src/pages/Assets/Tokens/ManageTokenPopup.tsx index 08e0e2eebc..8759bf82d5 100644 --- a/src/pages/Assets/Tokens/ManageTokenPopup.tsx +++ b/src/pages/Assets/Tokens/ManageTokenPopup.tsx @@ -1,6 +1,8 @@ import { ChangeEvent, useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' -import { FilledButton } from 'src/components/Button' +import ic_empty from 'src/assets/icons/ic_empty.svg' +import { FilledButton, OutlinedButton } from 'src/components/Button' +import ButtonHelper from 'src/components/ButtonHelper' import Checkbox from 'src/components/Input/Checkbox' import SearchInput from 'src/components/Input/Search' import { Popup } from 'src/components/Popup' @@ -8,6 +10,7 @@ import Header from 'src/components/Popup/Header' import { updateSafe } from 'src/logic/safe/store/actions/updateSafe' import { currentSafeWithNames } from 'src/logic/safe/store/selectors' import styled from 'styled-components' +import ic_close from 'src/assets/icons/ic_close.svg' const Wrap = styled.div` width: 480px; @@ -39,19 +42,30 @@ const Row = styled.div` justify-content: space-between; align-items: center; ` -export default function ManageTokenPopup({ open, onClose }) { + +export default function ManageTokenPopup({ + open, + onImport, + onClose, + keepMountedManagePopup, + setKeepMoutedManagePopup, +}) { const dispatch = useDispatch() const [toggleAll, setToggleAll] = useState(false) const { coinConfig, address } = useSelector(currentSafeWithNames) const [config, setConfig] = useState(coinConfig) + const applyHandler = () => { - dispatch( - updateSafe({ - address, - coinConfig: config, - }), - ) + if (config && config?.length > 0) { + dispatch( + updateSafe({ + address, + coinConfig: config, + }), + ) + } onClose() + setKeepMoutedManagePopup(false) } useEffect(() => { @@ -75,36 +89,69 @@ export default function ManageTokenPopup({ open, onClose }) { setConfig(filteredTokens) } + const handleDeleteCoin = (address: string) => { + const updatedConfig = config?.filter((coin) => coin.address !== address) + setConfig(updatedConfig) + } + useEffect(() => { - const isSelectAll = config?.every((token) => token?.enable) - setToggleAll(!!isSelectAll) + if (config && config?.length > 0) { + const isSelectAll = config?.every((token) => token?.enable) + setToggleAll(!!isSelectAll) + } }, [config]) return ( - -
+ +
{ + onClose() + setConfig(coinConfig) + setKeepMoutedManagePopup(false) + }} + hideNetwork={true} + />
Token list
-
- -
+ {config && config?.length > 0 ? ( + <> +
+ +
+ + ) : ( + <> + )}
- {config?.map((c, i) => { - return ( - setConfig(config.map((cf, id) => (i == id ? { ...cf, enable: !cf.enable } : cf)))} - /> - ) - })} + {config && config?.length > 0 ? ( + <> + {config?.map((c, i) => { + return ( + + setConfig(config.map((cf, id) => (i == id ? { ...cf, enable: !cf.enable } : cf))) + } + /> + ) + })} + + ) : ( + { + onClose() + onImport() + }} + /> + )}
@@ -125,12 +172,64 @@ const CoinWrapper = styled.div` > div { text-transform: uppercase; } + .actions { + display: flex; + align-items: center; + .btn-delete { + margin-right: 8px; + } + } ` -const CoinConfig = ({ name, isEnable, setToggle, type }) => { +const CoinConfig = ({ setToggle, coin, onDelete }) => { return ( -
{name}
- {type !== 'native' ? setToggle()} /> : <>} +
{coin.name}
+
+ {coin.isAddedToken ? ( +
+ onDelete(coin.address)}> + Trash Icon + +
+ ) : ( + <> + )} + {coin.type !== 'native' ? setToggle()} /> : <>} +
) } + +const EmptyWrapper = styled.div` + display: flex; + flex-direction: column; + align-items: center; + padding: 0px; + gap: 8px; + .title { + font-weight: 600; + font-size: 14px; + line-height: 18px; + } + .sub-title { + font-weight: 400; + font-size: 14px; + line-height: 18px; + } + .btn-import { + margin-top: 8px; + margin-bottom: 24px; + } +` +const Empty = ({ onImport }) => { + return ( + + +
This token hasn’t imported
+
Do you want to imported this token?
+ + Import + +
+ ) +} diff --git a/src/pages/Assets/Tokens/index.tsx b/src/pages/Assets/Tokens/index.tsx index 1477537192..e03d417adf 100644 --- a/src/pages/Assets/Tokens/index.tsx +++ b/src/pages/Assets/Tokens/index.tsx @@ -1,17 +1,17 @@ -import { ReactElement, useState } from 'react' -import styled from 'styled-components' -import SearchIcon from 'src/assets/icons/search.svg' -import { FilledButton, OutlinedNeutralButton } from 'src/components/Button' -import DenseTable, { StyledTableCell, StyledTableRow } from 'src/components/Table/DenseTable' +import { ChangeEvent, ReactElement, useEffect, useState } from 'react' import { useSelector } from 'react-redux' -import { extendedSafeTokensSelector } from 'src/utils/safeUtils/selector' -import { formatNativeCurrency, formatWithComma } from 'src/utils' -import SendingPopup from 'src/components/Popup/SendingPopup' import sendIcon from 'src/assets/icons/ArrowUpRight.svg' -import ManageTokenPopup from './ManageTokenPopup' +import { FilledButton, OutlinedNeutralButton } from 'src/components/Button' import SearchInput from 'src/components/Input/Search' +import SendingPopup from 'src/components/Popup/SendingPopup' +import DenseTable, { StyledTableCell, StyledTableRow } from 'src/components/Table/DenseTable' import { currentSafeWithNames } from 'src/logic/safe/store/selectors' import { Token } from 'src/logic/tokens/store/model/token' +import { formatWithComma } from 'src/utils' +import { extendedSafeTokensSelector } from 'src/utils/safeUtils/selector' +import styled from 'styled-components' +import ImportTokenPopup from './ImportTokenPopup' +import ManageTokenPopup from './ManageTokenPopup' const Wrap = styled.div` background: ${(props) => props.theme.backgroundPrimary}; border-radius: 8px; @@ -66,68 +66,112 @@ const TokenType = styled.div` function Tokens(props): ReactElement { const [open, setOpen] = useState(false) const [manageTokenPopupOpen, setManageTokenPopupOpen] = useState(false) + const [keepMountedManagePopup, setKeepMoutedManagePopup] = useState(true) + const [importTokenPopup, setImportTokenPopup] = useState(false) const [selectedToken, setSelectedToken] = useState('') const safeTokens: any = useSelector(extendedSafeTokensSelector) - const { coinConfig, address } = useSelector(currentSafeWithNames) + const { coinConfig } = useSelector(currentSafeWithNames) + const tokenConfig = safeTokens.filter((token) => { + return ( + token.type == 'native' || + coinConfig?.find((coin) => { + return coin.address == token.address + })?.enable + ) + }) + const [listToken, setListToken] = useState(tokenConfig) + + useEffect(() => { + setListToken(tokenConfig) + }, [coinConfig, safeTokens]) + + const handleSearch = (event: ChangeEvent) => { + const searchTerm = event.target.value.toLowerCase() + const filteredTokens = tokenConfig?.filter((token) => { + return token?.name?.toLowerCase().includes(searchTerm) || token?.address?.toLowerCase().includes(searchTerm) + }) + setListToken(filteredTokens) + } return (
Token list
- - setManageTokenPopupOpen(true)}> + + { + setManageTokenPopupOpen(true) + setKeepMoutedManagePopup(true) + }} + > Manage token
- {safeTokens - .filter((token) => { - return ( - token.type == 'native' || - coinConfig?.find((coin) => { - return coin.address == token.address - })?.enable - ) - }) - .map((token: Token, index: number) => { - return ( - - - - - {token.name || 'Unkonwn token'} - - - - {token.type == 'native' ? '' : token.type} - - {formatWithComma(token.balance.tokenBalance)} - -
- { - setOpen(true) - setSelectedToken(token?.address) - }} - > - - Send - - - - Receive - -
-
-
- ) - })} + {listToken.map((token: Token, index: number) => { + return ( + + + + + {token.name || 'Unkonwn token'} + + + + {token.type == 'native' ? '' : token.type} + + {formatWithComma(token.balance.tokenBalance)} + +
+ { + setOpen(true) + setSelectedToken(token?.address) + }} + > + + Send + + + + Receive + +
+
+
+ ) + })}
{}} onClose={() => setOpen(false)} /> - setManageTokenPopupOpen(false)} /> + {keepMountedManagePopup && ( + { + setImportTokenPopup(true) + }} + onClose={() => { + setManageTokenPopupOpen(false) + }} + keepMountedManagePopup={keepMountedManagePopup} + setKeepMoutedManagePopup={setKeepMoutedManagePopup} + /> + )} + { + setImportTokenPopup(false) + setManageTokenPopupOpen(true) + setKeepMoutedManagePopup(true) + }} + onClose={() => { + setImportTokenPopup(false) + setKeepMoutedManagePopup(false) + }} + />
) } diff --git a/src/pages/SmartContract/ContractInteraction/ManageTokenPopup.tsx b/src/pages/SmartContract/ContractInteraction/ManageTokenPopup.tsx index 0290d2e084..a55494f22a 100644 --- a/src/pages/SmartContract/ContractInteraction/ManageTokenPopup.tsx +++ b/src/pages/SmartContract/ContractInteraction/ManageTokenPopup.tsx @@ -141,7 +141,7 @@ const ManageTokenPopup = ({ open, onClose, setFunds, listTokens, setListTokens, }, [listTokens]) return ( - +
diff --git a/src/pages/Voting/ReviewTxPopup.tsx b/src/pages/Voting/ReviewTxPopup.tsx index 60d4063a35..f7d3a1c44c 100644 --- a/src/pages/Voting/ReviewTxPopup.tsx +++ b/src/pages/Voting/ReviewTxPopup.tsx @@ -41,7 +41,7 @@ const ReviewTxPopup = ({ open, onClose, proposal, vote, onBack, gasUsed }: Revie const chainDefaultGasPrice = getChainDefaultGasPrice() const decimal = getCoinDecimal() const [defaultGas, setDefaultGas] = useState( - chainDefaultGas.find((chain) => chain.typeUrl === MsgTypeUrl.Vote)?.gasAmount || DEFAULT_GAS_LIMIT.toString(), + chainDefaultGas?.find((chain) => chain.typeUrl === MsgTypeUrl.Vote)?.gasAmount || DEFAULT_GAS_LIMIT.toString(), ) const gasFee = defaultGas && chainDefaultGasPrice diff --git a/src/services/index.ts b/src/services/index.ts index 997660bdb0..0ff0e89e83 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -26,6 +26,7 @@ export interface ISafeCancel { interface IResponse { AdditionalData: any[] Data: any + data: any ErrorCode: string Message: string } @@ -286,3 +287,15 @@ export async function getContract(contractAddress: string, internalChainId: any) export async function getTokenDetail() { return fetch(githubPageTokenRegistryUrl) } + +export async function getDetailToken(address: string): Promise> { + const currentChainInfo = getChainInfo() as any + const { chainInfo } = await getGatewayUrl() + return axios + .get( + `${ + chainInfo.find((chain) => chain.chainId == currentChainInfo.chainId)?.rest + }/cosmwasm/wasm/v1/contract/${address}/smart/eyAidG9rZW5faW5mbyI6IHt9IH0%3D`, + ) + .then((res) => res.data) +} From 42bd80f9ad9c4b423f8c15ac353f76e3594ca32a Mon Sep 17 00:00:00 2001 From: HoangNDM6 Date: Mon, 12 Jun 2023 10:44:16 +0700 Subject: [PATCH 38/69] fix enable native token --- src/logic/tokens/store/actions/fetchSafeTokens.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/logic/tokens/store/actions/fetchSafeTokens.ts b/src/logic/tokens/store/actions/fetchSafeTokens.ts index d321035b66..abdbf22f49 100644 --- a/src/logic/tokens/store/actions/fetchSafeTokens.ts +++ b/src/logic/tokens/store/actions/fetchSafeTokens.ts @@ -135,6 +135,7 @@ export const fetchMSafeTokens = symbol: chainInfo.nativeCurrency.symbol, denom: chainInfo.denom, type: 'native', + enable: true, } balances.push(nativeToken) filteredListTokens.unshift(nativeToken) From 70003b8347d7dd0b1f12137a413bbe9db0c910c3 Mon Sep 17 00:00:00 2001 From: HoangNDM6 Date: Mon, 12 Jun 2023 10:48:36 +0700 Subject: [PATCH 39/69] fix toggle enable --- src/pages/Assets/Tokens/ManageTokenPopup.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/pages/Assets/Tokens/ManageTokenPopup.tsx b/src/pages/Assets/Tokens/ManageTokenPopup.tsx index 8759bf82d5..5324958255 100644 --- a/src/pages/Assets/Tokens/ManageTokenPopup.tsx +++ b/src/pages/Assets/Tokens/ManageTokenPopup.tsx @@ -73,7 +73,15 @@ export default function ManageTokenPopup({ }, [address]) const toggleAllHandler = () => { - setConfig(config?.map((cf, ii) => ({ ...cf, enable: !toggleAll }))) + setConfig( + config?.map((cf, ii) => { + if (cf.type === 'native') { + return cf + } else { + return { ...cf, enable: !toggleAll } + } + }), + ) setToggleAll(!toggleAll) } From 439362299a755ff9bcad61506706a7f69a0fd58e Mon Sep 17 00:00:00 2001 From: HoangNDM6 Date: Mon, 12 Jun 2023 17:02:39 +0700 Subject: [PATCH 40/69] fix import token --- src/components/JsonschemaForm/FundForm.tsx | 4 +- src/logic/safe/store/models/safe.ts | 2 + src/pages/Assets/Tokens/ImportTokenPopup.tsx | 19 +++-- src/pages/Assets/Tokens/ManageTokenPopup.tsx | 73 ++++++++++---------- src/pages/Assets/Tokens/index.tsx | 32 ++++++++- src/routes/LoadSafePage/LoadSafePage.tsx | 4 +- 6 files changed, 82 insertions(+), 52 deletions(-) diff --git a/src/components/JsonschemaForm/FundForm.tsx b/src/components/JsonschemaForm/FundForm.tsx index 7be9a0bc53..b90ca226c7 100644 --- a/src/components/JsonschemaForm/FundForm.tsx +++ b/src/components/JsonschemaForm/FundForm.tsx @@ -102,7 +102,9 @@ const FundForm = ({ fund, onDelete, onChangeAmount }: IFundFormProps) => { /> onDelete(fund.id)}> - } title="Delete"> + + Trash Icon + {amountValidateMsg && ( diff --git a/src/logic/safe/store/models/safe.ts b/src/logic/safe/store/models/safe.ts index fac0f69676..b167e4876f 100644 --- a/src/logic/safe/store/models/safe.ts +++ b/src/logic/safe/store/models/safe.ts @@ -47,6 +47,7 @@ export type SafeRecordProps = { nextQueueSeq: string sequence: string coinConfig?: any[] + isHideZeroBalance?: boolean } /** @@ -76,6 +77,7 @@ const makeSafe = Record({ nextQueueSeq: '1', sequence: '1', coinConfig: [], + isHideZeroBalance: true, }) export type SafeRecord = RecordOf diff --git a/src/pages/Assets/Tokens/ImportTokenPopup.tsx b/src/pages/Assets/Tokens/ImportTokenPopup.tsx index 37ed8d87b4..e91a60f325 100644 --- a/src/pages/Assets/Tokens/ImportTokenPopup.tsx +++ b/src/pages/Assets/Tokens/ImportTokenPopup.tsx @@ -6,10 +6,9 @@ import TextField from 'src/components/Input/TextField' import Loader from 'src/components/Loader' import { Popup } from 'src/components/Popup' import Header from 'src/components/Popup/Header' -import { getInternalChainId } from 'src/config' import { updateSafe } from 'src/logic/safe/store/actions/updateSafe' import { currentSafeWithNames } from 'src/logic/safe/store/selectors' -import { getContract, getDetailToken } from 'src/services' +import { getDetailToken } from 'src/services' import { isValidAddress } from 'src/utils/isValidAddress' import styled from 'styled-components' @@ -52,23 +51,21 @@ const defaultToken = { const ImportTokenPopup = ({ open, onBack, onClose }) => { const dispatch = useDispatch() - const internalChainId = getInternalChainId() const [token, setToken] = useState(defaultToken) const { coinConfig, address } = useSelector(currentSafeWithNames) const [isVerifiedContract, setIsVerifiedContract] = useState(null) const getContractDetail = async () => { setIsVerifiedContract('loading') - const { Data } = await getContract(token.address, internalChainId) - const { data } = await getDetailToken(token.address) - if (Data) { - setIsVerifiedContract(Data.verification ? 'true' : 'false') - } else { + try { + const { data } = await getDetailToken(token.address) + if (data) { + setIsVerifiedContract('true') + setToken({ ...token, name: data.name, symbol: data.symbol, decimals: data.decimals }) + } + } catch (error) { setIsVerifiedContract('false') } - if (data) { - setToken({ ...token, name: data.name, symbol: data.symbol, decimals: data.decimals }) - } } useEffect(() => { diff --git a/src/pages/Assets/Tokens/ManageTokenPopup.tsx b/src/pages/Assets/Tokens/ManageTokenPopup.tsx index 5324958255..586915a0cc 100644 --- a/src/pages/Assets/Tokens/ManageTokenPopup.tsx +++ b/src/pages/Assets/Tokens/ManageTokenPopup.tsx @@ -1,5 +1,7 @@ +import { Button, Tooltip } from '@material-ui/core' import { ChangeEvent, useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' +import ic_close from 'src/assets/icons/ic_close.svg' import ic_empty from 'src/assets/icons/ic_empty.svg' import { FilledButton, OutlinedButton } from 'src/components/Button' import ButtonHelper from 'src/components/ButtonHelper' @@ -10,7 +12,6 @@ import Header from 'src/components/Popup/Header' import { updateSafe } from 'src/logic/safe/store/actions/updateSafe' import { currentSafeWithNames } from 'src/logic/safe/store/selectors' import styled from 'styled-components' -import ic_close from 'src/assets/icons/ic_close.svg' const Wrap = styled.div` width: 480px; @@ -36,6 +37,9 @@ const Wrap = styled.div` overflow: auto; } } + .note { + margin-top: 8px; + } ` const Row = styled.div` display: flex; @@ -51,7 +55,6 @@ export default function ManageTokenPopup({ setKeepMoutedManagePopup, }) { const dispatch = useDispatch() - const [toggleAll, setToggleAll] = useState(false) const { coinConfig, address } = useSelector(currentSafeWithNames) const [config, setConfig] = useState(coinConfig) @@ -72,19 +75,6 @@ export default function ManageTokenPopup({ setConfig(coinConfig) }, [address]) - const toggleAllHandler = () => { - setConfig( - config?.map((cf, ii) => { - if (cf.type === 'native') { - return cf - } else { - return { ...cf, enable: !toggleAll } - } - }), - ) - setToggleAll(!toggleAll) - } - const handleSearch = (event: ChangeEvent) => { const searchTerm = event.target.value.toLowerCase() const filteredTokens = coinConfig?.filter((token) => { @@ -102,13 +92,6 @@ export default function ManageTokenPopup({ setConfig(updatedConfig) } - useEffect(() => { - if (config && config?.length > 0) { - const isSelectAll = config?.every((token) => token?.enable) - setToggleAll(!!isSelectAll) - } - }, [config]) - return (
- +
Token list
- {config && config?.length > 0 ? ( - <> -
- -
- - ) : ( - <> - )} +
{config && config?.length > 0 ? ( @@ -161,6 +145,9 @@ export default function ManageTokenPopup({ /> )}
+
+ Note: Zero balances are auto-hidden & You can only delete the tokens that you have imported manually +
@@ -180,6 +167,15 @@ const CoinWrapper = styled.div` > div { text-transform: uppercase; } + .info { + display: flex; + align-items: center; + .icon { + width: 20px; + height: 20px; + margin-right: 8px; + } + } .actions { display: flex; align-items: center; @@ -191,12 +187,17 @@ const CoinWrapper = styled.div` const CoinConfig = ({ setToggle, coin, onDelete }) => { return ( -
{coin.name}
+
+ +
{coin.name}
+
{coin.isAddedToken ? (
onDelete(coin.address)}> - Trash Icon + + Trash Icon +
) : ( @@ -233,8 +234,8 @@ const Empty = ({ onImport }) => { return ( -
This token hasn’t imported
-
Do you want to imported this token?
+
This token hasn’t been imported
+
Do you want to import this token?
Import diff --git a/src/pages/Assets/Tokens/index.tsx b/src/pages/Assets/Tokens/index.tsx index e03d417adf..dfca0e0329 100644 --- a/src/pages/Assets/Tokens/index.tsx +++ b/src/pages/Assets/Tokens/index.tsx @@ -1,5 +1,5 @@ import { ChangeEvent, ReactElement, useEffect, useState } from 'react' -import { useSelector } from 'react-redux' +import { useDispatch, useSelector } from 'react-redux' import sendIcon from 'src/assets/icons/ArrowUpRight.svg' import { FilledButton, OutlinedNeutralButton } from 'src/components/Button' import SearchInput from 'src/components/Input/Search' @@ -12,6 +12,8 @@ import { extendedSafeTokensSelector } from 'src/utils/safeUtils/selector' import styled from 'styled-components' import ImportTokenPopup from './ImportTokenPopup' import ManageTokenPopup from './ManageTokenPopup' +import Checkbox from 'src/components/Input/Checkbox' +import { updateSafe } from 'src/logic/safe/store/actions/updateSafe' const Wrap = styled.div` background: ${(props) => props.theme.backgroundPrimary}; border-radius: 8px; @@ -63,14 +65,23 @@ const TokenType = styled.div` background: #3d3730; } ` +const CheckboxWrapper = styled.div` + display: flex; + align-items: center; + .label { + margin-left: 8px; + } +` function Tokens(props): ReactElement { + const dispatch = useDispatch() const [open, setOpen] = useState(false) const [manageTokenPopupOpen, setManageTokenPopupOpen] = useState(false) const [keepMountedManagePopup, setKeepMoutedManagePopup] = useState(true) const [importTokenPopup, setImportTokenPopup] = useState(false) const [selectedToken, setSelectedToken] = useState('') const safeTokens: any = useSelector(extendedSafeTokensSelector) - const { coinConfig } = useSelector(currentSafeWithNames) + const { address, coinConfig, isHideZeroBalance } = useSelector(currentSafeWithNames) + const [hideZeroBalance, setHideZeroBalance] = useState(isHideZeroBalance ?? true) const tokenConfig = safeTokens.filter((token) => { return ( token.type == 'native' || @@ -93,11 +104,28 @@ function Tokens(props): ReactElement { setListToken(filteredTokens) } + const filterListToken = () => { + setHideZeroBalance(!hideZeroBalance) + const filteredList = listToken.filter((token) => token.balance.tokenBalance !== 0) + setListToken(filteredList) + dispatch( + updateSafe({ + address, + isHideZeroBalance: !hideZeroBalance, + }), + ) + } + return (
Token list
+ + +
Hide zero balances
+
+ - Safe Aura + Safe Aura Add existing Safe From 5c24abcba364904e92dd0c942d95c2c7cefd8d20 Mon Sep 17 00:00:00 2001 From: HoangNDM6 Date: Mon, 12 Jun 2023 22:53:18 +0700 Subject: [PATCH 41/69] fix default icon --- src/assets/icons/aura.png | Bin 0 -> 255488 bytes src/pages/Assets/Tokens/ImportTokenPopup.tsx | 3 +++ src/pages/Assets/Tokens/ManageTokenPopup.tsx | 4 ++-- 3 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 src/assets/icons/aura.png diff --git a/src/assets/icons/aura.png b/src/assets/icons/aura.png new file mode 100644 index 0000000000000000000000000000000000000000..c46eaea1a3bc8552306f789f265716ed5ac02bfd GIT binary patch literal 255488 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�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%0Dw2tAs+lgJX?s}0=XX&>0r2oCbA zEJ=nn0+@I#sPgibHrm~K`cTG?-H8_#AwjgrJ>AoO^4M;p=78h9vmCSQ``t%mC+V$^ z2S>bI%nF?(a#6DFCq;veWhMF~d273K^(`(@3q{<>bBFwQ?>sVLnp5j-4eqypeXbJQ z(Gk*>J|Kox>jGyV;ONPw;??1`3Q+0f|LYVft3F+-p{Ik#r2qoQq`N+8tb5Dbv2uVXHFksYQa^^7lDfck<`a~y&1lB1$S5tyBi5$zT_87Av4hprva70 z=kT|_!*^;V;MH%p!n(_|?gO`i45z=|;n{E`+?rJ#;-C~?)LcGGrbBNK*xR}2b5BdM zH*q}tb<#fNF=S85aMw<>TG-495DTT#_dj&)AYiN(3DCPey@nbeNva4Z6PRT%_SxbW z8b&rMMshx^NS*Sy?%O^?Q6F^$RT3DEG-&YjBD7~}Om1dI>=7#rw0l8>GN1qZIvf!( zS`joamdUBv;n0!EIhTz&SYS+%4r~RRHh}Pa@htbR7AeR^V;KHdf3T{*X3o9%Clzpc z_Gm$#KX<~$hbISTDO_2zX9@0MpMJ{OQb1>^-(o2GEfLeZbq=i05^FfAZ7j`J zFsD9O;3YiALAG5p+WS5hr!_5c@;Nqx@I%et zDi41dbY5ewHYbwvK(^x#pz3P663YH#zn6GvM%~Xr9zM1mefU=``1nJ~XO)fa$U_=@ zjFgs=vJd%`z(ufdVLjrRdu5&FzHw+w?8hmcPJ93BX~AxtQ>39+E>zzO|Jg3(5ZQs~ zf2}-UJ_L?oCA)Pw2_8KP%e5y1b0(KXyL)bdB!Mw#;uBC9ibyG=v!S4JT_k=bh2HPY ziqd+;&VW+8NzIqZS(M1SQ0J8R7^-HmKTCO?8M%sc)+y%nSKgW(*nF4G88bWXJZ6f=@7T8vXZfEaTEQ+zJb6h* z%81hlL}hQIe%|kOV{N{^KLI2IF1jBEMg%%ph|*s8hk{3=cR_Nz`f@<#$7Za3!sP6+ zH2GKoAXf>I^#8=|gxuN>!IU#t(6SaXX@NKR_e)_mbwMBD7i*=6mO2QNywj!@-Z!^{)rai&cdID%tNU1|$)w_}n*@=<0+s0`m_k zKlfz2KDoX%DL0?UDcxQ+ru4CX^jbT7)Z0#q&4R&AJ)v22NF6V%4$Z~5A}W5qTOffJ zVeJq@BNw&5T#*uxpPot|cYe3mv12R+QN7ob`iGnKfcY(bGNJP(nYf7l`tsdc??*nI z0#VNcqJqs`>ogEkKg#%%_QKfriF;|miTr+9tGgc}Ow$Fa8O z9~jMYNANG#fkF0Tw156nwhxI)tYrKs#;;`S#ZA7Sf~xAxARuSTaw}2atwcIkd$BXW z0CtUXns~?Z{X+g>?NBTqYqK(tECfsP!30{SE0X))-W`aqjhz9+z@Jn+4`|Mr#TZ>0d(6$+yfkx!_p1fTI46D^MyJej9{+N zE7rxBr*Ht0EYBS`0D#3+*_J#;H7~lUeNSTgi|Y+t`(`2Lgz$Ih_|lJmoNL@8MeuSk z1Ox7ab{~!onB}V1L^$a+k4Dk@#R}OtjbfttDQD zDFw^cCS=K{#ui8jRyo@lwTGG>p#qjhu`5)Rb7fb1Kn2814%`Yqt>kukeU&BMEM$(7 zd7+Y!yCgLHs+}E&SYi=KXbKAku2Z&!JawF0c`6(vy1*mU-6Ykzj7ssci@`p1(X(i{_=RO@J2 z=esprbbZM4Mp-u#F2qly_#wc?`aT}oDvt%wMQ%0DipR@g5V>f1AxTic__c89_*+gB zt&6VJwMO0|_c><5WFLmsynffGQOYm!NmOz3&*MOcBoa?}+U81k!p|Q(XXiOF*(TRd zipf80W3gzN5pTC7E0(ZeIQXFo&~mxDS}wiEDz)%U+QgdeEIl9680#7$JSK z$NY|ga*Zv|c+)(K%+(Q@NAmkOpAL22Y7raU)JpaSD{Aj~->-C>qEvmcHeb{dgDPCeUwtS?|(kK*on9g#QL%`{ar8ptqd-re2TGa(p#-g>}RypNV7N1+- zco{x?JHd#)#TfWj5u~Sb0mLq>3jF+QFW}q$-mNj1Y|jf&>~-|-3r!0s(NaZRD1qM6 zHk(}!8j1;hUV@sMP|g}kZ=lo|6?V(C z*wFMq{?*4jC2S_zOwPo)9|36-IY`@lY!oMZKE8eIWBBlnM76Si#q3FUfjKVHo9WU) z0}5TU07aa#fUQZYvt{3eeBPF^#Z$`ra~A%KTJOTqC668S{s-$bFH-b09M|5_Omo1b zKj`}j{DFc@NGR6}H~T;h#@TrTxQX7%Gn6J64{&40@Hy3?l)FYgKD5#I@Jt}E613*C z4+W~xH9I<7xei&6C$YmeC2Cer

9+^3t#00!FQww0ENExcO+xY{=^Ds4F%>$@pxQ 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 literal 0 HcmV?d00001 diff --git a/src/pages/Assets/Tokens/ImportTokenPopup.tsx b/src/pages/Assets/Tokens/ImportTokenPopup.tsx index e91a60f325..bb6d5635ba 100644 --- a/src/pages/Assets/Tokens/ImportTokenPopup.tsx +++ b/src/pages/Assets/Tokens/ImportTokenPopup.tsx @@ -11,6 +11,7 @@ import { currentSafeWithNames } from 'src/logic/safe/store/selectors' import { getDetailToken } from 'src/services' import { isValidAddress } from 'src/utils/isValidAddress' import styled from 'styled-components' +import ic_defIcon from 'src/assets/icons/aura.png' const Wrap = styled.div` width: 480px; @@ -39,6 +40,7 @@ type IToken = { isAddedToken: boolean enable: boolean decimals: number + logoUri: string } const defaultToken = { address: '', @@ -47,6 +49,7 @@ const defaultToken = { enable: false, isAddedToken: true, decimals: 0, + logoUri: ic_defIcon, } const ImportTokenPopup = ({ open, onBack, onClose }) => { diff --git a/src/pages/Assets/Tokens/ManageTokenPopup.tsx b/src/pages/Assets/Tokens/ManageTokenPopup.tsx index 586915a0cc..3fc34df933 100644 --- a/src/pages/Assets/Tokens/ManageTokenPopup.tsx +++ b/src/pages/Assets/Tokens/ManageTokenPopup.tsx @@ -171,8 +171,8 @@ const CoinWrapper = styled.div` display: flex; align-items: center; .icon { - width: 20px; - height: 20px; + width: 24px; + height: 24px; margin-right: 8px; } } From f72fb9f6f2908e2610ce026f6dcdb23f22c2428b Mon Sep 17 00:00:00 2001 From: HoangNDM6 Date: Tue, 13 Jun 2023 03:07:29 +0700 Subject: [PATCH 42/69] handle hide zero balance --- src/pages/Assets/Tokens/index.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/pages/Assets/Tokens/index.tsx b/src/pages/Assets/Tokens/index.tsx index dfca0e0329..28cf721e9f 100644 --- a/src/pages/Assets/Tokens/index.tsx +++ b/src/pages/Assets/Tokens/index.tsx @@ -90,7 +90,7 @@ function Tokens(props): ReactElement { })?.enable ) }) - const [listToken, setListToken] = useState(tokenConfig) + const [listToken, setListToken] = useState(tokenConfig.filter((token) => token.balance.tokenBalance !== 0)) useEffect(() => { setListToken(tokenConfig) @@ -104,10 +104,9 @@ function Tokens(props): ReactElement { setListToken(filteredTokens) } - const filterListToken = () => { + const handleFilterListToken = () => { setHideZeroBalance(!hideZeroBalance) - const filteredList = listToken.filter((token) => token.balance.tokenBalance !== 0) - setListToken(filteredList) + setListToken(!hideZeroBalance ? tokenConfig.filter((token) => token.balance.tokenBalance !== 0) : tokenConfig) dispatch( updateSafe({ address, @@ -122,7 +121,7 @@ function Tokens(props): ReactElement {

- +
Hide zero balances
From ea40196d13c934f345e6bebb05c288b3b77e6b6f Mon Sep 17 00:00:00 2001 From: HoangNDM6 Date: Tue, 13 Jun 2023 08:58:10 +0700 Subject: [PATCH 43/69] fix hide zero balances --- src/pages/Assets/Tokens/index.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pages/Assets/Tokens/index.tsx b/src/pages/Assets/Tokens/index.tsx index 28cf721e9f..efdb33249d 100644 --- a/src/pages/Assets/Tokens/index.tsx +++ b/src/pages/Assets/Tokens/index.tsx @@ -81,7 +81,7 @@ function Tokens(props): ReactElement { const [selectedToken, setSelectedToken] = useState('') const safeTokens: any = useSelector(extendedSafeTokensSelector) const { address, coinConfig, isHideZeroBalance } = useSelector(currentSafeWithNames) - const [hideZeroBalance, setHideZeroBalance] = useState(isHideZeroBalance ?? true) + const [hideZeroBalance, setHideZeroBalance] = useState(isHideZeroBalance) const tokenConfig = safeTokens.filter((token) => { return ( token.type == 'native' || @@ -90,7 +90,9 @@ function Tokens(props): ReactElement { })?.enable ) }) - const [listToken, setListToken] = useState(tokenConfig.filter((token) => token.balance.tokenBalance !== 0)) + const [listToken, setListToken] = useState( + isHideZeroBalance ? tokenConfig.filter((token) => token.balance.tokenBalance !== 0) : tokenConfig, + ) useEffect(() => { setListToken(tokenConfig) From 8ee025f8bfaefe7ee585728e552874a3a8d70b9e Mon Sep 17 00:00:00 2001 From: HoangNDM6 Date: Tue, 13 Jun 2023 09:47:13 +0700 Subject: [PATCH 44/69] fix hide zero balances --- src/pages/Assets/Tokens/index.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/pages/Assets/Tokens/index.tsx b/src/pages/Assets/Tokens/index.tsx index efdb33249d..88a6a2f8f8 100644 --- a/src/pages/Assets/Tokens/index.tsx +++ b/src/pages/Assets/Tokens/index.tsx @@ -95,8 +95,8 @@ function Tokens(props): ReactElement { ) useEffect(() => { - setListToken(tokenConfig) - }, [coinConfig, safeTokens]) + setListToken(hideZeroBalance ? tokenConfig.filter((token) => token.balance.tokenBalance !== 0) : tokenConfig) + }, [coinConfig, safeTokens, hideZeroBalance]) const handleSearch = (event: ChangeEvent) => { const searchTerm = event.target.value.toLowerCase() @@ -108,7 +108,6 @@ function Tokens(props): ReactElement { const handleFilterListToken = () => { setHideZeroBalance(!hideZeroBalance) - setListToken(!hideZeroBalance ? tokenConfig.filter((token) => token.balance.tokenBalance !== 0) : tokenConfig) dispatch( updateSafe({ address, From 54bc2561674dc0795eb8fa5ee55c27aca06fe3f4 Mon Sep 17 00:00:00 2001 From: HoangNDM6 Date: Tue, 13 Jun 2023 14:34:01 +0700 Subject: [PATCH 45/69] fix token icon --- src/logic/tokens/store/actions/fetchSafeTokens.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/logic/tokens/store/actions/fetchSafeTokens.ts b/src/logic/tokens/store/actions/fetchSafeTokens.ts index abdbf22f49..346eb7abfb 100644 --- a/src/logic/tokens/store/actions/fetchSafeTokens.ts +++ b/src/logic/tokens/store/actions/fetchSafeTokens.ts @@ -158,9 +158,7 @@ export const fetchMSafeTokens = tokenBalance: `${humanReadableValue(+data?.amount > 0 ? data?.amount : 0, tokenDetail?.decimals || 6)}`, tokenAddress: tokenDetail?.address, decimals: tokenDetail?.decimals || 6, - logoUri: tokenDetail?.icon - ? `https://aura-nw.github.io/token-registry/images/${tokenDetail?.icon}` - : 'https://aura-nw.github.io/token-registry/images/undefined.png', + logoUri: tokenDetail?.icon ?? 'https://aura-nw.github.io/token-registry/images/undefined.png', name: tokenDetail?.name, symbol: tokenDetail?.coinDenom, denom: tokenDetail?.minCoinDenom, @@ -177,9 +175,7 @@ export const fetchMSafeTokens = tokenAddress: tokenDetail?.address, decimals: tokenDetail?.decimals || 6, name: tokenDetail?.name, - logoUri: tokenDetail?.icon - ? `https://aura-nw.github.io/token-registry/images/${tokenDetail?.icon}` - : 'https://aura-nw.github.io/token-registry/images/undefined.png', + logoUri: tokenDetail?.icon ?? 'https://aura-nw.github.io/token-registry/images/undefined.png', symbol: tokenDetail?.symbol, denom: tokenDetail?.symbol, type: 'CW20', From b725522c236b2f521c586439b30c83f22e71a1ef Mon Sep 17 00:00:00 2001 From: imhson Date: Tue, 13 Jun 2023 16:27:12 +0700 Subject: [PATCH 46/69] fix imported token is not shown --- .../tokens/store/actions/fetchSafeTokens.ts | 28 +++++++++++++++---- src/pages/Assets/Tokens/ImportTokenPopup.tsx | 2 +- src/pages/Assets/Tokens/index.tsx | 1 - 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/logic/tokens/store/actions/fetchSafeTokens.ts b/src/logic/tokens/store/actions/fetchSafeTokens.ts index 346eb7abfb..d77349bfc7 100644 --- a/src/logic/tokens/store/actions/fetchSafeTokens.ts +++ b/src/logic/tokens/store/actions/fetchSafeTokens.ts @@ -110,7 +110,19 @@ export const fetchMSafeTokens = const listChain = getChains() const tokenDetailsListData = await getTokenDetail() const tokenDetailsList = await tokenDetailsListData.json() - listTokens = [...tokenDetailsList['ibc'], ...tokenDetailsList['cw20']] + listTokens = [ + ...tokenDetailsList['ibc'], + ...tokenDetailsList['cw20'], + // ...(safe?.coinConfig?.find((c) => c.isAddedToken)), + ] + const importedConfig = + safe?.coinConfig?.filter((c) => { + if (c.isAddedToken) { + return !listTokens.some((t) => t.address === c.address) + } + return false + }) || [] + listTokens = [...listTokens, ...importedConfig] const filteredListTokens = listTokens.map((token) => { const isExist = listSafeTokens.some((t) => t.denom === token.minCoinDenom || t.address === token.address) if (isExist) { @@ -153,12 +165,15 @@ export const fetchMSafeTokens = safeInfo.balance .filter((balance) => balance.denom != chainInfo.denom) .forEach((data: any) => { - const tokenDetail = tokenDetailsList['ibc'].find((token) => token.cosmosDenom == data.minimal_denom) + const tokenDetail = listTokens.find((token) => token.cosmosDenom == data.minimal_denom) balances.push({ tokenBalance: `${humanReadableValue(+data?.amount > 0 ? data?.amount : 0, tokenDetail?.decimals || 6)}`, tokenAddress: tokenDetail?.address, decimals: tokenDetail?.decimals || 6, - logoUri: tokenDetail?.icon ?? 'https://aura-nw.github.io/token-registry/images/undefined.png', + logoUri: + tokenDetail?.icon || + tokenDetail?.logoUri || + 'https://aura-nw.github.io/token-registry/images/undefined.png', name: tokenDetail?.name, symbol: tokenDetail?.coinDenom, denom: tokenDetail?.minCoinDenom, @@ -169,13 +184,16 @@ export const fetchMSafeTokens = if (safeInfo.assets.CW20.asset.length > 0) { safeInfo.assets.CW20.asset.forEach((data) => { - const tokenDetail = tokenDetailsList['cw20'].find((token) => token.address == data.contract_address) + const tokenDetail = listTokens.find((token) => token.address == data.contract_address) balances.push({ tokenBalance: `${humanReadableValue(+data?.balance > 0 ? data?.balance : 0, tokenDetail?.decimals || 6)}`, tokenAddress: tokenDetail?.address, decimals: tokenDetail?.decimals || 6, name: tokenDetail?.name, - logoUri: tokenDetail?.icon ?? 'https://aura-nw.github.io/token-registry/images/undefined.png', + logoUri: + tokenDetail?.icon || + tokenDetail?.logoUri || + 'https://aura-nw.github.io/token-registry/images/undefined.png', symbol: tokenDetail?.symbol, denom: tokenDetail?.symbol, type: 'CW20', diff --git a/src/pages/Assets/Tokens/ImportTokenPopup.tsx b/src/pages/Assets/Tokens/ImportTokenPopup.tsx index bb6d5635ba..fff015dc0c 100644 --- a/src/pages/Assets/Tokens/ImportTokenPopup.tsx +++ b/src/pages/Assets/Tokens/ImportTokenPopup.tsx @@ -46,7 +46,7 @@ const defaultToken = { address: '', symbol: '', name: '', - enable: false, + enable: true, isAddedToken: true, decimals: 0, logoUri: ic_defIcon, diff --git a/src/pages/Assets/Tokens/index.tsx b/src/pages/Assets/Tokens/index.tsx index 88a6a2f8f8..6e172bd8bd 100644 --- a/src/pages/Assets/Tokens/index.tsx +++ b/src/pages/Assets/Tokens/index.tsx @@ -97,7 +97,6 @@ function Tokens(props): ReactElement { useEffect(() => { setListToken(hideZeroBalance ? tokenConfig.filter((token) => token.balance.tokenBalance !== 0) : tokenConfig) }, [coinConfig, safeTokens, hideZeroBalance]) - const handleSearch = (event: ChangeEvent) => { const searchTerm = event.target.value.toLowerCase() const filteredTokens = tokenConfig?.filter((token) => { From 757cd355543a3ff3442ac775878de90a753adf66 Mon Sep 17 00:00:00 2001 From: HoangNDM6 Date: Wed, 14 Jun 2023 14:03:25 +0700 Subject: [PATCH 47/69] filter list options asset --- src/components/Input/Token/index.tsx | 16 +++++++++++++--- .../tokens/store/actions/fetchSafeTokens.ts | 12 ++---------- src/pages/Assets/Tokens/ImportTokenPopup.tsx | 5 +++++ 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/components/Input/Token/index.tsx b/src/components/Input/Token/index.tsx index 9f03e275fd..8b1cb93b6f 100644 --- a/src/components/Input/Token/index.tsx +++ b/src/components/Input/Token/index.tsx @@ -1,6 +1,7 @@ import MenuItem from '@material-ui/core/MenuItem' import { useSelector } from 'react-redux' import Select, { IOption } from 'src/components/Input/Select' +import { currentSafeWithNames } from 'src/logic/safe/store/selectors' import { Token } from 'src/logic/tokens/store/model/token' import { extendedSafeTokensSelector } from 'src/utils/safeUtils/selector' import styled from 'styled-components' @@ -15,9 +16,18 @@ const MenuItemWrapper = styled.div` } ` export default function TokenSelect({ selectedToken, setSelectedToken, disabled = false }) { - const tokenList = useSelector(extendedSafeTokensSelector) as unknown as Token[] + const tokenList: any = useSelector(extendedSafeTokensSelector) + const { coinConfig } = useSelector(currentSafeWithNames) + const tokenConfig = tokenList.filter((token) => { + return ( + token.type == 'native' || + coinConfig?.find((coin) => { + return coin.address == token.address + })?.enable + ) + }) - const tokenOptions: IOption[] = tokenList.map((token: Token) => ({ + const tokenOptions: IOption[] = tokenConfig.map((token: Token) => ({ value: token.address, label: token.name, })) as IOption[] @@ -41,7 +51,7 @@ export default function TokenSelect({ selectedToken, setSelectedToken, disabled ) : null }} > - {tokenList.map((token: Token, index: any) => { + {tokenConfig.map((token: any, index: any) => { return ( diff --git a/src/logic/tokens/store/actions/fetchSafeTokens.ts b/src/logic/tokens/store/actions/fetchSafeTokens.ts index d77349bfc7..1d488bcda2 100644 --- a/src/logic/tokens/store/actions/fetchSafeTokens.ts +++ b/src/logic/tokens/store/actions/fetchSafeTokens.ts @@ -110,11 +110,7 @@ export const fetchMSafeTokens = const listChain = getChains() const tokenDetailsListData = await getTokenDetail() const tokenDetailsList = await tokenDetailsListData.json() - listTokens = [ - ...tokenDetailsList['ibc'], - ...tokenDetailsList['cw20'], - // ...(safe?.coinConfig?.find((c) => c.isAddedToken)), - ] + listTokens = [...tokenDetailsList['ibc'], ...tokenDetailsList['cw20']] const importedConfig = safe?.coinConfig?.filter((c) => { if (c.isAddedToken) { @@ -125,11 +121,7 @@ export const fetchMSafeTokens = listTokens = [...listTokens, ...importedConfig] const filteredListTokens = listTokens.map((token) => { const isExist = listSafeTokens.some((t) => t.denom === token.minCoinDenom || t.address === token.address) - if (isExist) { - return { ...token, enable: true } - } else { - return { ...token, enable: false } - } + return { ...token, enable: isExist } }) const chainInfo: any = listChain.find((x: any) => x.internalChainId === safeInfo?.internalChainId) const nativeTokenData = safeInfo.balance.find((balance) => balance.denom == chainInfo.denom) diff --git a/src/pages/Assets/Tokens/ImportTokenPopup.tsx b/src/pages/Assets/Tokens/ImportTokenPopup.tsx index fff015dc0c..786d15b714 100644 --- a/src/pages/Assets/Tokens/ImportTokenPopup.tsx +++ b/src/pages/Assets/Tokens/ImportTokenPopup.tsx @@ -12,6 +12,8 @@ import { getDetailToken } from 'src/services' import { isValidAddress } from 'src/utils/isValidAddress' import styled from 'styled-components' import ic_defIcon from 'src/assets/icons/aura.png' +import { extractSafeAddress, extractSafeId } from 'src/routes/routes' +import { fetchMSafe } from 'src/logic/safe/store/actions/fetchSafe' const Wrap = styled.div` width: 480px; @@ -57,6 +59,8 @@ const ImportTokenPopup = ({ open, onBack, onClose }) => { const [token, setToken] = useState(defaultToken) const { coinConfig, address } = useSelector(currentSafeWithNames) const [isVerifiedContract, setIsVerifiedContract] = useState(null) + const safeAddress = extractSafeAddress() + const safeId = extractSafeId() as number const getContractDetail = async () => { setIsVerifiedContract('loading') @@ -104,6 +108,7 @@ const ImportTokenPopup = ({ open, onBack, onClose }) => { coinConfig: newCoinConfig ?? coinConfig, }), ) + dispatch(fetchMSafe(safeAddress, safeId)) onClose() setToken(defaultToken) } From d650156456cef4c8a63869c2f8a202614b74c914 Mon Sep 17 00:00:00 2001 From: HoangNDM6 Date: Wed, 14 Jun 2023 15:02:50 +0700 Subject: [PATCH 48/69] add field api send funds --- src/components/Popup/MultiSendPopup/CreateTxPopup.tsx | 1 + src/components/Popup/SendingPopup/CreateTxPopup.tsx | 1 + src/pages/Avanced/Custom Transaction/ReviewPopup.tsx | 1 + src/pages/SmartContract/ContractInteraction/ReviewPopup.tsx | 1 + src/pages/Staking/TxActionModal/ClaimReward.tsx | 1 + src/pages/Staking/TxActionModal/Delegate.tsx | 1 + src/pages/Staking/TxActionModal/Redelegate.tsx | 1 + src/pages/Staking/TxActionModal/Undelegate.tsx | 1 + src/pages/Voting/ReviewTxPopup.tsx | 1 + src/utils/signer.ts | 5 ++++- 10 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/components/Popup/MultiSendPopup/CreateTxPopup.tsx b/src/components/Popup/MultiSendPopup/CreateTxPopup.tsx index eb77e6ed98..98ffc83bf4 100644 --- a/src/components/Popup/MultiSendPopup/CreateTxPopup.tsx +++ b/src/components/Popup/MultiSendPopup/CreateTxPopup.tsx @@ -86,6 +86,7 @@ export default function CreateTxPopup({ msgs, manualGasLimit || '250000', sequence, + undefined, () => { setDisabled(true) }, diff --git a/src/components/Popup/SendingPopup/CreateTxPopup.tsx b/src/components/Popup/SendingPopup/CreateTxPopup.tsx index 9d593056fd..ec0a014849 100644 --- a/src/components/Popup/SendingPopup/CreateTxPopup.tsx +++ b/src/components/Popup/SendingPopup/CreateTxPopup.tsx @@ -94,6 +94,7 @@ export default function CreateTxPopup({ msgs, manualGasLimit, sequence, + recipient?.address, () => { setDisabled(true) }, diff --git a/src/pages/Avanced/Custom Transaction/ReviewPopup.tsx b/src/pages/Avanced/Custom Transaction/ReviewPopup.tsx index bbb9ed88a1..bf820138be 100644 --- a/src/pages/Avanced/Custom Transaction/ReviewPopup.tsx +++ b/src/pages/Avanced/Custom Transaction/ReviewPopup.tsx @@ -76,6 +76,7 @@ export default function ReviewPopup({ open, setOpen, gasUsed, msg }) { msg, manualGasLimit || '250000', sequence, + undefined, () => { setDisabled(true) }, diff --git a/src/pages/SmartContract/ContractInteraction/ReviewPopup.tsx b/src/pages/SmartContract/ContractInteraction/ReviewPopup.tsx index d11233d1f6..fdb51228ef 100644 --- a/src/pages/SmartContract/ContractInteraction/ReviewPopup.tsx +++ b/src/pages/SmartContract/ContractInteraction/ReviewPopup.tsx @@ -71,6 +71,7 @@ export default function ReviewPopup({ open, setOpen, gasUsed, data, contractData msgs, manualGasLimit || '250000', sequence, + undefined, () => { setDisabled(true) }, diff --git a/src/pages/Staking/TxActionModal/ClaimReward.tsx b/src/pages/Staking/TxActionModal/ClaimReward.tsx index cca885d912..378190c240 100644 --- a/src/pages/Staking/TxActionModal/ClaimReward.tsx +++ b/src/pages/Staking/TxActionModal/ClaimReward.tsx @@ -41,6 +41,7 @@ export default function ClaimReward({ listReward, onClose, gasUsed }) { msgs, manualGasLimit || '250000', sequence, + undefined, () => { setDisabled(true) }, diff --git a/src/pages/Staking/TxActionModal/Delegate.tsx b/src/pages/Staking/TxActionModal/Delegate.tsx index b4f87b63db..1c3cfa01dd 100644 --- a/src/pages/Staking/TxActionModal/Delegate.tsx +++ b/src/pages/Staking/TxActionModal/Delegate.tsx @@ -57,6 +57,7 @@ export default function Delegate({ validator, amount, onClose, gasUsed }) { msgs, manualGasLimit || '250000', sequence, + undefined, () => { setDisabled(true) }, diff --git a/src/pages/Staking/TxActionModal/Redelegate.tsx b/src/pages/Staking/TxActionModal/Redelegate.tsx index f61fbbc3ea..054573c5a9 100644 --- a/src/pages/Staking/TxActionModal/Redelegate.tsx +++ b/src/pages/Staking/TxActionModal/Redelegate.tsx @@ -67,6 +67,7 @@ export default function Redelegate({ validator, amount, onClose, dstValidator, g msgs, manualGasLimit || '250000', sequence, + undefined, () => { setDisabled(true) }, diff --git a/src/pages/Staking/TxActionModal/Undelegate.tsx b/src/pages/Staking/TxActionModal/Undelegate.tsx index 1cab4e08b1..1d7457e079 100644 --- a/src/pages/Staking/TxActionModal/Undelegate.tsx +++ b/src/pages/Staking/TxActionModal/Undelegate.tsx @@ -67,6 +67,7 @@ export default function Undelegate({ validator, amount, onClose, gasUsed }) { msgs, manualGasLimit || '250000', sequence, + undefined, () => { setDisabled(true) }, diff --git a/src/pages/Voting/ReviewTxPopup.tsx b/src/pages/Voting/ReviewTxPopup.tsx index f7d3a1c44c..2f3d05c16a 100644 --- a/src/pages/Voting/ReviewTxPopup.tsx +++ b/src/pages/Voting/ReviewTxPopup.tsx @@ -83,6 +83,7 @@ const ReviewTxPopup = ({ open, onClose, proposal, vote, onBack, gasUsed }: Revie msgs, manualGasLimit || '250000', sequence, + undefined, () => { setDisabled(true) }, diff --git a/src/utils/signer.ts b/src/utils/signer.ts index 42eff4fb77..dff33f2bd1 100644 --- a/src/utils/signer.ts +++ b/src/utils/signer.ts @@ -52,6 +52,7 @@ export const signAndCreateTransaction = message: any[], gasLimit: string, sequence: string, + toAddress?: string, beforeSigningCallback?: () => void, successSigningCallback?: () => void, errorSigningCallback?: (error: any) => void, @@ -109,6 +110,9 @@ export const signAndCreateTransaction = accountNumber: signResult.accountNumber, sequence: signResult.sequence, } + if (toAddress) { + data.to = toAddress + } const result = await createSafeTransaction(data) const { ErrorCode } = result if (ErrorCode === MESSAGES_CODE.SUCCESSFUL.ErrorCode) { @@ -440,4 +444,3 @@ const signMessage = async ( throw new Error(error) } } - From eeed71f1f1d0891eb3a9926521f22de046534e23 Mon Sep 17 00:00:00 2001 From: imhson Date: Wed, 14 Jun 2023 17:13:33 +0700 Subject: [PATCH 49/69] fix loading tx detail --- src/logic/safe/store/actions/fetchTransactionDetails.ts | 4 ++-- src/pages/Transactions/components/TxDetail/Message.tsx | 4 +++- src/pages/Transactions/components/TxQuickAction.tsx | 2 +- src/utils/hooks/useTransactionDetails.ts | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/logic/safe/store/actions/fetchTransactionDetails.ts b/src/logic/safe/store/actions/fetchTransactionDetails.ts index 943b2225e7..a7cf05dc08 100644 --- a/src/logic/safe/store/actions/fetchTransactionDetails.ts +++ b/src/logic/safe/store/actions/fetchTransactionDetails.ts @@ -19,8 +19,8 @@ export const fetchTransactionDetailsById = ({ transactionId, auraTxId }: { transactionId?: string; auraTxId?: string }) => async (dispatch: Dispatch, getState: () => AppReduxState): Promise => { const transaction = getTransactionByAttribute(getState(), { - attributeValue: transactionId, - attributeName: 'id', + attributeValue: auraTxId, + attributeName: 'auraTxId', }) const safeAddress = extractSafeAddress() const chainId = currentChainId(getState()) diff --git a/src/pages/Transactions/components/TxDetail/Message.tsx b/src/pages/Transactions/components/TxDetail/Message.tsx index 9145b14b8d..d6d9479a80 100644 --- a/src/pages/Transactions/components/TxDetail/Message.tsx +++ b/src/pages/Transactions/components/TxDetail/Message.tsx @@ -195,7 +195,9 @@ export default function TxMsg({ tx, txDetail }) { } return (
-
+ {txDetail?.rawMessage && ( +
+ )}
) } diff --git a/src/pages/Transactions/components/TxQuickAction.tsx b/src/pages/Transactions/components/TxQuickAction.tsx index 696e4bb84b..9aaf5ef790 100644 --- a/src/pages/Transactions/components/TxQuickAction.tsx +++ b/src/pages/Transactions/components/TxQuickAction.tsx @@ -37,7 +37,7 @@ export default function TxQuickAction({ transaction, curSeq }) { const [loading, setLoading] = useState(false) const [isWaiting, setIsWaiting] = useState(false) const data = useSelector((state: AppReduxState) => - getTransactionByAttribute(state, { attributeValue: transaction.id, attributeName: 'id' }), + getTransactionByAttribute(state, { attributeValue: transaction.auraTxId, attributeName: 'auraTxId' }), ) const dispatch = useDispatch() diff --git a/src/utils/hooks/useTransactionDetails.ts b/src/utils/hooks/useTransactionDetails.ts index afc73c6e34..5262f80371 100644 --- a/src/utils/hooks/useTransactionDetails.ts +++ b/src/utils/hooks/useTransactionDetails.ts @@ -16,7 +16,7 @@ export const useTransactionDetails = (transactionId?: string, txHash?: string, a data: undefined, }) const data = useSelector((state: AppReduxState) => - getTransactionByAttribute(state, { attributeValue: transactionId, attributeName: 'id' }), + getTransactionByAttribute(state, { attributeValue: auraTxId, attributeName: 'auraTxId' }), ) useEffect(() => { const dataTemp = { From b5fc1bab148672877f18322045d7c470c45c2c20 Mon Sep 17 00:00:00 2001 From: imhson Date: Wed, 14 Jun 2023 17:23:04 +0700 Subject: [PATCH 50/69] fix id tx --- src/logic/safe/store/actions/fetchTransactionDetails.ts | 4 ++-- src/pages/Transactions/components/TxQuickAction.tsx | 5 ++++- src/utils/hooks/useTransactionDetails.ts | 5 ++++- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/logic/safe/store/actions/fetchTransactionDetails.ts b/src/logic/safe/store/actions/fetchTransactionDetails.ts index a7cf05dc08..ddf7c2bbfd 100644 --- a/src/logic/safe/store/actions/fetchTransactionDetails.ts +++ b/src/logic/safe/store/actions/fetchTransactionDetails.ts @@ -19,8 +19,8 @@ export const fetchTransactionDetailsById = ({ transactionId, auraTxId }: { transactionId?: string; auraTxId?: string }) => async (dispatch: Dispatch, getState: () => AppReduxState): Promise => { const transaction = getTransactionByAttribute(getState(), { - attributeValue: auraTxId, - attributeName: 'auraTxId', + attributeValue: transactionId ? transactionId : auraTxId, + attributeName: transactionId ? 'id' : 'auraTxId', }) const safeAddress = extractSafeAddress() const chainId = currentChainId(getState()) diff --git a/src/pages/Transactions/components/TxQuickAction.tsx b/src/pages/Transactions/components/TxQuickAction.tsx index 9aaf5ef790..107cb8f399 100644 --- a/src/pages/Transactions/components/TxQuickAction.tsx +++ b/src/pages/Transactions/components/TxQuickAction.tsx @@ -37,7 +37,10 @@ export default function TxQuickAction({ transaction, curSeq }) { const [loading, setLoading] = useState(false) const [isWaiting, setIsWaiting] = useState(false) const data = useSelector((state: AppReduxState) => - getTransactionByAttribute(state, { attributeValue: transaction.auraTxId, attributeName: 'auraTxId' }), + getTransactionByAttribute(state, { + attributeValue: transaction.id ? transaction.id : transaction.auraTxId, + attributeName: transaction.id ? 'id' : 'auraTxId', + }), ) const dispatch = useDispatch() diff --git a/src/utils/hooks/useTransactionDetails.ts b/src/utils/hooks/useTransactionDetails.ts index 5262f80371..e130643b85 100644 --- a/src/utils/hooks/useTransactionDetails.ts +++ b/src/utils/hooks/useTransactionDetails.ts @@ -16,7 +16,10 @@ export const useTransactionDetails = (transactionId?: string, txHash?: string, a data: undefined, }) const data = useSelector((state: AppReduxState) => - getTransactionByAttribute(state, { attributeValue: auraTxId, attributeName: 'auraTxId' }), + getTransactionByAttribute(state, { + attributeValue: transactionId ? transactionId : auraTxId, + attributeName: transactionId ? 'id' : 'auraTxId', + }), ) useEffect(() => { const dataTemp = { From b60f14b168bc779f72fa9fb79585e48319c95c29 Mon Sep 17 00:00:00 2001 From: HoangNDM6 Date: Thu, 15 Jun 2023 10:05:08 +0700 Subject: [PATCH 51/69] fix list transaction --- .../Transactions/History/Transaction.tsx | 17 ++++++--- src/pages/Transactions/History/index.tsx | 5 ++- src/pages/Transactions/Queue/Transaction.tsx | 11 ++++-- src/pages/Transactions/Queue/index.tsx | 7 ++-- .../Transactions/components/TxAmount.tsx | 14 +++++-- .../components/TxDetail/Message.tsx | 38 +++++++++---------- src/pages/Transactions/components/TxType.tsx | 9 ++--- src/types/transaction.d.ts | 1 + src/utils/transactionUtils.ts | 2 + 9 files changed, 63 insertions(+), 41 deletions(-) diff --git a/src/pages/Transactions/History/Transaction.tsx b/src/pages/Transactions/History/Transaction.tsx index cf24697696..eb48b67786 100644 --- a/src/pages/Transactions/History/Transaction.tsx +++ b/src/pages/Transactions/History/Transaction.tsx @@ -1,15 +1,20 @@ import { AccordionDetails } from '@aura/safe-react-components' +import { useState } from 'react' import { formatTimeInWords } from 'src/utils/date' import TxAmount from '../components/TxAmount' +import TxDetail from '../components/TxDetail' +import TxSequence from '../components/TxSequence' import TxStatus from '../components/TxStatus' import TxTime from '../components/TxTime' import TxType from '../components/TxType' import { NoPaddingAccordion, StyledAccordionSummary, StyledTransaction } from '../styled' -import { useEffect, useState } from 'react' -import TxDetail from '../components/TxDetail' -import TxSequence from '../components/TxSequence' -export default function Transaction({ transaction, notFirstTx }) { +export default function Transaction({ transaction, notFirstTx, listTokens }) { const [txDetailLoaded, setTxDetailLoaded] = useState(false) + + const token = listTokens.find( + (t) => t.cosmosDenom === transaction.txInfo.denom || t.denom === transaction.txInfo.denom, + ) + if (!transaction) { return null } @@ -30,8 +35,8 @@ export default function Transaction({ transaction, notFirstTx }) { ) : ( )} - - + + diff --git a/src/pages/Transactions/History/index.tsx b/src/pages/Transactions/History/index.tsx index fca1a8aa0d..8e7a2ff1c6 100644 --- a/src/pages/Transactions/History/index.tsx +++ b/src/pages/Transactions/History/index.tsx @@ -1,10 +1,12 @@ import { Loader, Title } from '@aura/safe-react-components' import { Fragment, ReactElement, useEffect } from 'react' +import { useSelector } from 'react-redux' import NoTransactionsImage from 'src/assets/icons/no-transactions.svg' import Img from 'src/components/layout/Img' import { useQuery } from 'src/utils' import { formatWithSchema } from 'src/utils/date' +import { extendedSafeTokensSelector } from 'src/utils/safeUtils/selector' import { usePagedHistoryTransactions } from '../../../utils/hooks/usePagedHistoryTransactions' import { AccordionWrapper, @@ -17,9 +19,9 @@ import { import Transaction from './Transaction' export default function HistoryTransactions(): ReactElement { const { count, isLoading, hasMore, next, transactions: historyTx } = usePagedHistoryTransactions() - const queryParams = useQuery() const transactionId = queryParams.get('transactionId') + const listTokens: any = useSelector(extendedSafeTokensSelector) const expandTx = () => { const elem = document.getElementById(`tx-${transactionId}`) as any @@ -77,6 +79,7 @@ export default function HistoryTransactions(): ReactElement { className="history-tx" > diff --git a/src/pages/Transactions/Queue/Transaction.tsx b/src/pages/Transactions/Queue/Transaction.tsx index 506df37a64..7f0cb15133 100644 --- a/src/pages/Transactions/Queue/Transaction.tsx +++ b/src/pages/Transactions/Queue/Transaction.tsx @@ -1,5 +1,5 @@ import { AccordionDetails } from '@aura/safe-react-components' -import { useEffect, useMemo, useRef, useState } from 'react' +import { useState } from 'react' import { formatTimeInWords } from 'src/utils/date' import TxAmount from '../components/TxAmount' import TxDetail from '../components/TxDetail' @@ -14,16 +14,21 @@ export default function Transaction({ transaction, hideSeq, curSeq, + listTokens, }: { transaction: any hideSeq?: boolean curSeq: string + listTokens?: any }) { const [txDetailLoaded, setTxDetailLoaded] = useState(false) if (!transaction) { return null } + const token = listTokens.find( + (t) => t.cosmosDenom === transaction.txInfo.denom || t.denom === transaction.txInfo.denom, + ) return ( )} - - + + {`Queued - Transaction with sequence ${curSeq} needs to be executed first`}

) : null} - + ) : ( @@ -129,7 +130,7 @@ export default function QueueTransactions(): ReactElement {

{txs.map((tx, index) => ( - + ))} diff --git a/src/pages/Transactions/components/TxAmount.tsx b/src/pages/Transactions/components/TxAmount.tsx index be609644e5..2f4f07b416 100644 --- a/src/pages/Transactions/components/TxAmount.tsx +++ b/src/pages/Transactions/components/TxAmount.tsx @@ -1,14 +1,20 @@ import { getNativeCurrency } from 'src/config' -import { formatNativeToken } from 'src/utils' +import { convertAmount } from 'src/utils' -export default function TxAmount({ amount = 0 }) { +type TxAmountProps = { + amount: number + token?: any +} +export default function TxAmount({ amount = 0, token }: TxAmountProps) { const nativeCurrency = getNativeCurrency() return (
{amount ? ( <> - native-url-icon -

{formatNativeToken(amount)}

+ native-url-icon +

+ {convertAmount(amount, false, token?.decimals)} {token?.symbol} +

) : ( `-` diff --git a/src/pages/Transactions/components/TxDetail/Message.tsx b/src/pages/Transactions/components/TxDetail/Message.tsx index 9145b14b8d..bf17206db8 100644 --- a/src/pages/Transactions/components/TxDetail/Message.tsx +++ b/src/pages/Transactions/components/TxDetail/Message.tsx @@ -1,11 +1,11 @@ -import { MsgTypeUrl } from 'src/logic/providers/constants/constant' -import { beutifyJson, convertAmount, formatNativeToken } from 'src/utils' -import AddressInfo from 'src/components/AddressInfo' import { Fragment, useEffect, useState } from 'react' -import { formatDateTime, formatWithSchema } from 'src/utils/date' +import AddressInfo from 'src/components/AddressInfo' +import { Message } from 'src/components/CustomTransactionMessage/SmallMsg' import StatusCard from 'src/components/StatusCard' +import { MsgTypeUrl } from 'src/logic/providers/constants/constant' +import { beutifyJson, convertAmount, formatNativeToken } from 'src/utils' +import { formatWithSchema } from 'src/utils/date' import styled from 'styled-components' -import { Message } from 'src/components/CustomTransactionMessage/SmallMsg' const voteMapping = { 1: 'Yes', @@ -41,20 +41,20 @@ export default function TxMsg({ tx, txDetail }) { ) } if (type == MsgTypeUrl.ExecuteContract) { - // if (txDetail?.txMessage[0].contractFunction == 'transfer') { - // return ( - //
- // - // Send{' '} - // - // {convertAmount(JSON.parse(txDetail?.txMessage[0].contractArgs)?.amount || '0', false)} - // {' '} - // to: - // - // - //
- // ) - // } + if (txDetail?.txMessage[0].contractFunction === 'transfer') { + return ( +
+ + Send{' '} + + {convertAmount(JSON.parse(txDetail?.txMessage[0].contractArgs)?.amount || '0', false)} + {' '} + to: + + +
+ ) + } return (
diff --git a/src/pages/Transactions/components/TxType.tsx b/src/pages/Transactions/components/TxType.tsx index 0d358c8bf6..c829e61861 100644 --- a/src/pages/Transactions/components/TxType.tsx +++ b/src/pages/Transactions/components/TxType.tsx @@ -1,9 +1,8 @@ -import { CustomIconText } from 'src/components/CustomIconText' -import { MsgTypeUrl } from 'src/logic/providers/constants/constant' +import { Icon } from '@aura/safe-react-components' +import CustomIcon from 'src/assets/icons/custom.svg' import IncomingIcon from 'src/assets/icons/incoming.svg' import OutgoingIcon from 'src/assets/icons/outgoing.svg' -import CustomIcon from 'src/assets/icons/custom.svg' -import { Icon, IconTypes } from '@aura/safe-react-components' +import { MsgTypeUrl } from 'src/logic/providers/constants/constant' export default function TxType({ type }) { if (type == MsgTypeUrl.Delegate) { @@ -23,7 +22,7 @@ export default function TxType({ type }) {
) } - if (type == MsgTypeUrl.Send) { + if (type == MsgTypeUrl.Send || type === 'Send') { return (
outgoing-icon diff --git a/src/types/transaction.d.ts b/src/types/transaction.d.ts index d7d256045a..349b4d9c92 100644 --- a/src/types/transaction.d.ts +++ b/src/types/transaction.d.ts @@ -59,6 +59,7 @@ export interface ITransactionListItem { TypeUrl?: string FinalAmount?: number Timestamp?: number + DisplayType?: string } export interface ISignature { diff --git a/src/utils/transactionUtils.ts b/src/utils/transactionUtils.ts index f475c387ba..e41bb51718 100644 --- a/src/utils/transactionUtils.ts +++ b/src/utils/transactionUtils.ts @@ -95,6 +95,7 @@ const makeTransactions = (list: ITransactionListItem[]): MTransactionListItem[] type: 'Transfer', typeUrl: tx?.TypeUrl, amount: tx?.FinalAmount, + denom: tx?.Denom, sender: { value: tx?.FromAddress, name: null, @@ -110,6 +111,7 @@ const makeTransactions = (list: ITransactionListItem[]): MTransactionListItem[] type: TokenType.NATIVE_COIN, value: tx?.Amount?.toString(), }, + displayType: tx?.DisplayType, }, }, } From d8b0b1139575fa50d4f2335c6c738d515b16dcba Mon Sep 17 00:00:00 2001 From: HoangNDM6 Date: Thu, 15 Jun 2023 14:15:33 +0700 Subject: [PATCH 52/69] fix transaction receive --- .../Transactions/components/TxDetail/Message.tsx | 6 ++++++ .../Transactions/components/TxDetail/index.tsx | 16 +++++++++++----- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/pages/Transactions/components/TxDetail/Message.tsx b/src/pages/Transactions/components/TxDetail/Message.tsx index 3a5101239a..2fb3275472 100644 --- a/src/pages/Transactions/components/TxDetail/Message.tsx +++ b/src/pages/Transactions/components/TxDetail/Message.tsx @@ -41,6 +41,9 @@ export default function TxMsg({ tx, txDetail }) { ) } if (type == MsgTypeUrl.ExecuteContract) { + if (tx.txInfo.displayType === 'Receive') { + return <> + } if (txDetail?.txMessage[0].contractFunction === 'transfer') { return (
@@ -110,6 +113,9 @@ export default function TxMsg({ tx, txDetail }) { ) } if (type == MsgTypeUrl.Send) { + if (tx.txInfo.displayType === 'Receive') { + return <> + } return (
diff --git a/src/pages/Transactions/components/TxDetail/index.tsx b/src/pages/Transactions/components/TxDetail/index.tsx index c9b6ad3197..eeaba73479 100644 --- a/src/pages/Transactions/components/TxDetail/index.tsx +++ b/src/pages/Transactions/components/TxDetail/index.tsx @@ -62,11 +62,17 @@ export default function TxDetail({ transaction, isHistoryTx }) {
- - {!data.executor && isOwner && !isHistoryTx && ( -
- -
+ {transaction.txInfo.displayType !== 'Receive' ? ( + <> + + {!data.executor && isOwner && !isHistoryTx && ( +
+ +
+ )} + + ) : ( + <> )}
From 05da9662aff6b56c49224d99ef7fbc0a0498d3c1 Mon Sep 17 00:00:00 2001 From: HoangNDM6 Date: Thu, 15 Jun 2023 14:52:35 +0700 Subject: [PATCH 53/69] fix detail transaction --- src/pages/Transactions/History/Transaction.tsx | 4 +++- src/pages/Transactions/Queue/Transaction.tsx | 2 +- .../Transactions/components/TxDetail/Message.tsx | 12 ++++++++---- src/pages/Transactions/components/TxDetail/index.tsx | 4 ++-- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/pages/Transactions/History/Transaction.tsx b/src/pages/Transactions/History/Transaction.tsx index eb48b67786..71edf4d936 100644 --- a/src/pages/Transactions/History/Transaction.tsx +++ b/src/pages/Transactions/History/Transaction.tsx @@ -41,7 +41,9 @@ export default function Transaction({ transaction, notFirstTx, listTokens }) { - {txDetailLoaded && } + + {txDetailLoaded && } + ) } diff --git a/src/pages/Transactions/Queue/Transaction.tsx b/src/pages/Transactions/Queue/Transaction.tsx index 7f0cb15133..3dfcba6412 100644 --- a/src/pages/Transactions/Queue/Transaction.tsx +++ b/src/pages/Transactions/Queue/Transaction.tsx @@ -60,7 +60,7 @@ export default function Transaction({ - {txDetailLoaded && } + {txDetailLoaded && } ) diff --git a/src/pages/Transactions/components/TxDetail/Message.tsx b/src/pages/Transactions/components/TxDetail/Message.tsx index 2fb3275472..e4642bce81 100644 --- a/src/pages/Transactions/components/TxDetail/Message.tsx +++ b/src/pages/Transactions/components/TxDetail/Message.tsx @@ -21,9 +21,9 @@ const StyledStatus = styled.div` padding: 0; } ` -export default function TxMsg({ tx, txDetail }) { +export default function TxMsg({ tx, txDetail, token }) { const type = tx.txInfo.typeUrl - const amount = formatNativeToken(txDetail.txMessage[0]?.amount || 0) + const amount = convertAmount(txDetail.txMessage[0]?.amount || 0, false, token?.decimals) const [msg, setMsg] = useState([]) useEffect(() => { if (txDetail?.rawMessage) { @@ -50,7 +50,7 @@ export default function TxMsg({ tx, txDetail }) { Send{' '} - {convertAmount(JSON.parse(txDetail?.txMessage[0].contractArgs)?.amount || '0', false)} + {convertAmount(JSON.parse(txDetail?.txMessage[0].contractArgs)?.amount || '0', false)} {token?.symbol} {' '} to: @@ -119,7 +119,11 @@ export default function TxMsg({ tx, txDetail }) { return (
- Send {amount} to: + Send{' '} + + {amount} {token?.symbol} + {' '} + to:
diff --git a/src/pages/Transactions/components/TxDetail/index.tsx b/src/pages/Transactions/components/TxDetail/index.tsx index eeaba73479..afdb368490 100644 --- a/src/pages/Transactions/components/TxDetail/index.tsx +++ b/src/pages/Transactions/components/TxDetail/index.tsx @@ -8,7 +8,7 @@ import { Centered, InlineEthHashInfo, TxDetailsContainer } from '../../styled' import { TxActions } from './Action' import TxMsg from './Message' import { TxOwners } from './Owner' -export default function TxDetail({ transaction, isHistoryTx }) { +export default function TxDetail({ transaction, isHistoryTx, token }) { const isOwner = useSelector(grantedSelector) const { data, loading } = useTransactionDetails(transaction.id, transaction.txHash, transaction.auraTxId) @@ -58,7 +58,7 @@ export default function TxDetail({ transaction, isHistoryTx }) {

)}
- +
From 53a9467ac42384c3326abedf6717f807ef06e8a5 Mon Sep 17 00:00:00 2001 From: HoangNDM6 Date: Thu, 15 Jun 2023 16:23:36 +0700 Subject: [PATCH 54/69] fix filter zero balances --- src/pages/Assets/Tokens/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/Assets/Tokens/index.tsx b/src/pages/Assets/Tokens/index.tsx index 6e172bd8bd..fcb07348b7 100644 --- a/src/pages/Assets/Tokens/index.tsx +++ b/src/pages/Assets/Tokens/index.tsx @@ -91,11 +91,11 @@ function Tokens(props): ReactElement { ) }) const [listToken, setListToken] = useState( - isHideZeroBalance ? tokenConfig.filter((token) => token.balance.tokenBalance !== 0) : tokenConfig, + isHideZeroBalance ? tokenConfig.filter((token) => token.balance.tokenBalance > 0) : tokenConfig, ) useEffect(() => { - setListToken(hideZeroBalance ? tokenConfig.filter((token) => token.balance.tokenBalance !== 0) : tokenConfig) + setListToken(hideZeroBalance ? tokenConfig.filter((token) => token.balance.tokenBalance > 0) : tokenConfig) }, [coinConfig, safeTokens, hideZeroBalance]) const handleSearch = (event: ChangeEvent) => { const searchTerm = event.target.value.toLowerCase() From f5d4141e2a38cf223a696f62ec0168fd0d6ec5dc Mon Sep 17 00:00:00 2001 From: HoangNDM6 Date: Thu, 15 Jun 2023 17:16:51 +0700 Subject: [PATCH 55/69] fix search token --- src/pages/Assets/Tokens/index.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/pages/Assets/Tokens/index.tsx b/src/pages/Assets/Tokens/index.tsx index fcb07348b7..f1b9635781 100644 --- a/src/pages/Assets/Tokens/index.tsx +++ b/src/pages/Assets/Tokens/index.tsx @@ -72,6 +72,7 @@ const CheckboxWrapper = styled.div` margin-left: 8px; } ` +let updatedListTokens function Tokens(props): ReactElement { const dispatch = useDispatch() const [open, setOpen] = useState(false) @@ -95,11 +96,13 @@ function Tokens(props): ReactElement { ) useEffect(() => { - setListToken(hideZeroBalance ? tokenConfig.filter((token) => token.balance.tokenBalance > 0) : tokenConfig) + updatedListTokens = hideZeroBalance ? tokenConfig.filter((token) => token.balance.tokenBalance > 0) : tokenConfig + setListToken(updatedListTokens) }, [coinConfig, safeTokens, hideZeroBalance]) + const handleSearch = (event: ChangeEvent) => { const searchTerm = event.target.value.toLowerCase() - const filteredTokens = tokenConfig?.filter((token) => { + const filteredTokens = updatedListTokens?.filter((token) => { return token?.name?.toLowerCase().includes(searchTerm) || token?.address?.toLowerCase().includes(searchTerm) }) setListToken(filteredTokens) From 2f5159e523efcfda0b1032743af75809550bffa8 Mon Sep 17 00:00:00 2001 From: HoangNDM6 Date: Fri, 16 Jun 2023 09:55:02 +0700 Subject: [PATCH 56/69] fix search transaction --- src/pages/Assets/Tokens/index.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/pages/Assets/Tokens/index.tsx b/src/pages/Assets/Tokens/index.tsx index f1b9635781..2086b4adba 100644 --- a/src/pages/Assets/Tokens/index.tsx +++ b/src/pages/Assets/Tokens/index.tsx @@ -80,6 +80,7 @@ function Tokens(props): ReactElement { const [keepMountedManagePopup, setKeepMoutedManagePopup] = useState(true) const [importTokenPopup, setImportTokenPopup] = useState(false) const [selectedToken, setSelectedToken] = useState('') + const [search, setSearch] = useState('') const safeTokens: any = useSelector(extendedSafeTokensSelector) const { address, coinConfig, isHideZeroBalance } = useSelector(currentSafeWithNames) const [hideZeroBalance, setHideZeroBalance] = useState(isHideZeroBalance) @@ -97,11 +98,16 @@ function Tokens(props): ReactElement { useEffect(() => { updatedListTokens = hideZeroBalance ? tokenConfig.filter((token) => token.balance.tokenBalance > 0) : tokenConfig - setListToken(updatedListTokens) + setListToken( + updatedListTokens?.filter((token) => { + return token?.name?.toLowerCase().includes(search) || token?.address?.toLowerCase().includes(search) + }), + ) }, [coinConfig, safeTokens, hideZeroBalance]) const handleSearch = (event: ChangeEvent) => { const searchTerm = event.target.value.toLowerCase() + setSearch(searchTerm) const filteredTokens = updatedListTokens?.filter((token) => { return token?.name?.toLowerCase().includes(searchTerm) || token?.address?.toLowerCase().includes(searchTerm) }) From 55d54f1c056d30da0dc5ce0f6359514c042b83a9 Mon Sep 17 00:00:00 2001 From: HoangNDM6 Date: Fri, 16 Jun 2023 15:09:42 +0700 Subject: [PATCH 57/69] fix multi send --- src/components/Input/Token/index.tsx | 5 ++++- src/components/Popup/MultiSendPopup/index.tsx | 13 +++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/components/Input/Token/index.tsx b/src/components/Input/Token/index.tsx index 8b1cb93b6f..ce14292df3 100644 --- a/src/components/Input/Token/index.tsx +++ b/src/components/Input/Token/index.tsx @@ -15,7 +15,7 @@ const MenuItemWrapper = styled.div` margin-right: 8px; } ` -export default function TokenSelect({ selectedToken, setSelectedToken, disabled = false }) { +export default function TokenSelect({ selectedToken, setSelectedToken, disabled = false, onlyNativeToken = false }) { const tokenList: any = useSelector(extendedSafeTokensSelector) const { coinConfig } = useSelector(currentSafeWithNames) const tokenConfig = tokenList.filter((token) => { @@ -52,6 +52,9 @@ export default function TokenSelect({ selectedToken, setSelectedToken, disabled }} > {tokenConfig.map((token: any, index: any) => { + if (onlyNativeToken && token.type !== 'native') { + return null + } return ( diff --git a/src/components/Popup/MultiSendPopup/index.tsx b/src/components/Popup/MultiSendPopup/index.tsx index dc3601ea45..991b0af5b4 100644 --- a/src/components/Popup/MultiSendPopup/index.tsx +++ b/src/components/Popup/MultiSendPopup/index.tsx @@ -21,6 +21,7 @@ import Header from '../Header' import CreateTxPopup from './CreateTxPopup' import { BodyWrapper, Footer, PopupWrapper } from './styles' import Loader from 'src/components/Loader' +import { Token } from 'src/logic/tokens/store/model/token' export type RecipientProps = { amount: string @@ -46,13 +47,13 @@ const MultiSendPopup = ({ open, onClose, onOpen }: SendFundsProps): ReactElement const [addressValidateErrorMsg, setAddressValidateErrorMsg] = useState('') const [addressValidateSuccessMsg, setAddressValidateSuccessMsg] = useState('') const [amountValidateMsg, setAmountValidateMsg] = useState('') - const [selectedToken, setSelectedToken] = useState('') + const [selectedToken, setSelectedToken] = useState(undefined) const [rawRecipient, setRawRecipient] = useState('') const [totalAmount, setTotalAmount] = useState('0') const [balance, setBalance] = useState(0) useEffect(() => { - const bl = tokens.find((token) => token.address == selectedToken)?.balance?.tokenBalance || 0 + const bl = tokens.find((token) => token.address == selectedToken?.address)?.balance?.tokenBalance || 0 setBalance(+bl) }, [selectedToken]) @@ -74,7 +75,7 @@ const MultiSendPopup = ({ open, onClose, onOpen }: SendFundsProps): ReactElement return } onClose() - setSelectedToken('') + setSelectedToken(undefined) clearData() } @@ -175,7 +176,7 @@ const MultiSendPopup = ({ open, onClose, onOpen }: SendFundsProps): ReactElement
handleClose()} subTitle="Step 1 of 2" title="Multi-send" />
- +
Add recipients & amounts
@@ -201,7 +202,7 @@ const MultiSendPopup = ({ open, onClose, onOpen }: SendFundsProps): ReactElement {row.address} {row.amount} - ) + ) })} @@ -240,7 +241,7 @@ const MultiSendPopup = ({ open, onClose, onOpen }: SendFundsProps): ReactElement t.address == selectedToken)} + selectedToken={tokens.find((t) => t.address == selectedToken?.address)} open={createTxPopupOpen} handleClose={handleClose} gasUsed={String(Math.round(gasUsed * 1.3))} From c688b2430440ff5a7e25181954de0b5e412049c8 Mon Sep 17 00:00:00 2001 From: HoangNDM6 Date: Tue, 20 Jun 2023 14:37:40 +0700 Subject: [PATCH 58/69] fix msg too long --- src/utils/index.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/utils/index.ts b/src/utils/index.ts index 80d8cfe7ce..c707101e80 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -13,12 +13,15 @@ export const beutifyJson = (data) => { const formattedJson = json.replace( /("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) { - var cls = 'number' + let cls = 'number' if (/^"/.test(match)) { if (/:$/.test(match)) { cls = 'key' } else { cls = 'string' + if (match.length > 900) { + match = match.slice(0, 900) + '..."' + } } } else if (/true|false/.test(match)) { cls = 'boolean' From dab0916b7e5808a986b6a7d0446e83791d687852 Mon Sep 17 00:00:00 2001 From: HoangNDM6 Date: Wed, 21 Jun 2023 13:08:29 +0700 Subject: [PATCH 59/69] fix show token added --- src/components/Input/Select/index.tsx | 2 +- src/components/Popup/MultiSendPopup/index.tsx | 2 +- .../Popup/SendingPopup/CurrentSafe.tsx | 34 ++----------- src/components/Popup/SendingPopup/index.tsx | 2 +- .../tokens/store/actions/fetchSafeTokens.ts | 49 ++++++++++++++----- .../Avanced/Custom Transaction/index.tsx | 2 +- .../ContractInteraction/Contract.tsx | 2 +- src/pages/Staking/index.tsx | 2 +- 8 files changed, 45 insertions(+), 50 deletions(-) diff --git a/src/components/Input/Select/index.tsx b/src/components/Input/Select/index.tsx index 75d3aea002..d59527cd3f 100644 --- a/src/components/Input/Select/index.tsx +++ b/src/components/Input/Select/index.tsx @@ -122,7 +122,7 @@ const Select = ({ setIsOpen(true) }} disableUnderline - IconComponent={isOpen ? CaretUpIcon : CaretDownIcon} + IconComponent={!disabled ? (isOpen ? CaretUpIcon : CaretDownIcon) : () => null} MenuProps={menuProps} classes={{ select: classes.select, diff --git a/src/components/Popup/MultiSendPopup/index.tsx b/src/components/Popup/MultiSendPopup/index.tsx index 991b0af5b4..5b28e37e86 100644 --- a/src/components/Popup/MultiSendPopup/index.tsx +++ b/src/components/Popup/MultiSendPopup/index.tsx @@ -244,7 +244,7 @@ const MultiSendPopup = ({ open, onClose, onOpen }: SendFundsProps): ReactElement selectedToken={tokens.find((t) => t.address == selectedToken?.address)} open={createTxPopupOpen} handleClose={handleClose} - gasUsed={String(Math.round(gasUsed * 1.3))} + gasUsed={String(Math.round(gasUsed * 1.6))} /> ) diff --git a/src/components/Popup/SendingPopup/CurrentSafe.tsx b/src/components/Popup/SendingPopup/CurrentSafe.tsx index 3bd8f82874..4209813cc6 100644 --- a/src/components/Popup/SendingPopup/CurrentSafe.tsx +++ b/src/components/Popup/SendingPopup/CurrentSafe.tsx @@ -1,29 +1,11 @@ import { useSelector } from 'react-redux' -import styled from 'styled-components' -import { getExplorerInfo, getNativeCurrency } from 'src/config' -import { currentSafeWithNames } from 'src/logic/safe/store/selectors' -import Paragraph from 'src/components/layout/Paragraph' -import Bold from 'src/components/layout/Bold' -import { border, xs } from 'src/theme/variables' -import Block from 'src/components/layout/Block' import PrefixedEthHashInfo from 'src/components/PrefixedEthHashInfo' - -const StyledBlock = styled(Block)` - font-size: 12px; - line-height: 1.08; - letter-spacing: -0.5px; - background-color: ${border}; - width: fit-content; - padding: 5px 10px; - margin-top: ${xs}; - margin-left: 40px; - border-radius: 3px; -` +import { getExplorerInfo } from 'src/config' +import { currentSafeWithNames } from 'src/logic/safe/store/selectors' const CurrentSafe = (): React.ReactElement => { - const { address: safeAddress, nativeBalance, name: safeName } = useSelector(currentSafeWithNames) - const nativeCurrency = getNativeCurrency() + const { address: safeAddress, name: safeName } = useSelector(currentSafeWithNames) return ( <> @@ -34,16 +16,6 @@ const CurrentSafe = (): React.ReactElement => { showAvatar showCopyBtn /> - {nativeBalance && ( - - - Balance:{' '} - {`${parseFloat(nativeBalance).toFixed(6)} ${ - nativeCurrency.symbol - }`} - - - )} ) } diff --git a/src/components/Popup/SendingPopup/index.tsx b/src/components/Popup/SendingPopup/index.tsx index c957788b38..ca4a101172 100644 --- a/src/components/Popup/SendingPopup/index.tsx +++ b/src/components/Popup/SendingPopup/index.tsx @@ -223,7 +223,7 @@ const SendingPopup = ({ open, onClose, onOpen, defaultToken }: SendFundsProps): amount={amount} open={createTxPopupOpen} handleClose={handleClose} - gasUsed={String(Math.round(gasUsed * 1.3))} + gasUsed={String(Math.round(gasUsed * 1.6))} /> ) diff --git a/src/logic/tokens/store/actions/fetchSafeTokens.ts b/src/logic/tokens/store/actions/fetchSafeTokens.ts index 1d488bcda2..fedde7270a 100644 --- a/src/logic/tokens/store/actions/fetchSafeTokens.ts +++ b/src/logic/tokens/store/actions/fetchSafeTokens.ts @@ -177,19 +177,42 @@ export const fetchMSafeTokens = if (safeInfo.assets.CW20.asset.length > 0) { safeInfo.assets.CW20.asset.forEach((data) => { const tokenDetail = listTokens.find((token) => token.address == data.contract_address) - balances.push({ - tokenBalance: `${humanReadableValue(+data?.balance > 0 ? data?.balance : 0, tokenDetail?.decimals || 6)}`, - tokenAddress: tokenDetail?.address, - decimals: tokenDetail?.decimals || 6, - name: tokenDetail?.name, - logoUri: - tokenDetail?.icon || - tokenDetail?.logoUri || - 'https://aura-nw.github.io/token-registry/images/undefined.png', - symbol: tokenDetail?.symbol, - denom: tokenDetail?.symbol, - type: 'CW20', - }) + if (tokenDetail) { + balances.push({ + tokenBalance: `${humanReadableValue(+data?.balance > 0 ? data?.balance : 0, tokenDetail?.decimals || 6)}`, + tokenAddress: tokenDetail?.address, + decimals: tokenDetail?.decimals || 6, + name: tokenDetail?.name, + logoUri: + tokenDetail?.icon || + tokenDetail?.logoUri || + 'https://aura-nw.github.io/token-registry/images/undefined.png', + symbol: tokenDetail?.symbol, + denom: tokenDetail?.symbol, + type: 'CW20', + }) + } else { + listTokens.forEach((token) => { + if (token.tokenType !== 'ibc' && token.tokenType !== 'native') { + const isTokenInAsset = safeInfo.assets.CW20.asset.some( + (data) => data.contract_address === token.address, + ) + if (!isTokenInAsset) { + balances.push({ + tokenBalance: `${humanReadableValue(0, tokenDetail?.decimals || 6)}`, + tokenAddress: token?.address, + decimals: token?.decimals || 6, + name: token?.name, + logoUri: + token?.icon || token?.logoUri || 'https://aura-nw.github.io/token-registry/images/undefined.png', + symbol: token?.symbol, + denom: token?.symbol, + type: 'CW20', + }) + } + } + }) + } }) } diff --git a/src/pages/Avanced/Custom Transaction/index.tsx b/src/pages/Avanced/Custom Transaction/index.tsx index b08a73a641..756a2a8999 100644 --- a/src/pages/Avanced/Custom Transaction/index.tsx +++ b/src/pages/Avanced/Custom Transaction/index.tsx @@ -85,7 +85,7 @@ function CustomTransaction(props): ReactElement {
- + ) } diff --git a/src/pages/SmartContract/ContractInteraction/Contract.tsx b/src/pages/SmartContract/ContractInteraction/Contract.tsx index 701a6e16c7..389f17c1c5 100644 --- a/src/pages/SmartContract/ContractInteraction/Contract.tsx +++ b/src/pages/SmartContract/ContractInteraction/Contract.tsx @@ -159,7 +159,7 @@ function Contract({ contractData }): ReactElement { From 14a7eecb8107bf3e0d29ae00240f98e464486a49 Mon Sep 17 00:00:00 2001 From: HoangNDM6 Date: Thu, 22 Jun 2023 14:27:49 +0700 Subject: [PATCH 60/69] fix gasUsed --- src/components/Popup/MultiSendPopup/index.tsx | 2 +- src/components/Popup/SendingPopup/index.tsx | 2 +- src/pages/Assets/Tokens/ManageTokenPopup.tsx | 6 +++--- src/pages/Assets/Tokens/index.tsx | 5 ++++- src/pages/Avanced/Custom Transaction/index.tsx | 2 +- src/pages/SmartContract/ContractInteraction/Contract.tsx | 2 +- src/pages/Staking/index.tsx | 2 +- src/types/transaction.d.ts | 1 + src/utils/transactionUtils.ts | 1 + 9 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/components/Popup/MultiSendPopup/index.tsx b/src/components/Popup/MultiSendPopup/index.tsx index 5b28e37e86..a96f18931f 100644 --- a/src/components/Popup/MultiSendPopup/index.tsx +++ b/src/components/Popup/MultiSendPopup/index.tsx @@ -244,7 +244,7 @@ const MultiSendPopup = ({ open, onClose, onOpen }: SendFundsProps): ReactElement selectedToken={tokens.find((t) => t.address == selectedToken?.address)} open={createTxPopupOpen} handleClose={handleClose} - gasUsed={String(Math.round(gasUsed * 1.6))} + gasUsed={String(Math.round(gasUsed * 2))} /> ) diff --git a/src/components/Popup/SendingPopup/index.tsx b/src/components/Popup/SendingPopup/index.tsx index ca4a101172..fa3181375e 100644 --- a/src/components/Popup/SendingPopup/index.tsx +++ b/src/components/Popup/SendingPopup/index.tsx @@ -223,7 +223,7 @@ const SendingPopup = ({ open, onClose, onOpen, defaultToken }: SendFundsProps): amount={amount} open={createTxPopupOpen} handleClose={handleClose} - gasUsed={String(Math.round(gasUsed * 1.6))} + gasUsed={String(Math.round(gasUsed * 2))} /> ) diff --git a/src/pages/Assets/Tokens/ManageTokenPopup.tsx b/src/pages/Assets/Tokens/ManageTokenPopup.tsx index 3fc34df933..3c1e9d13a0 100644 --- a/src/pages/Assets/Tokens/ManageTokenPopup.tsx +++ b/src/pages/Assets/Tokens/ManageTokenPopup.tsx @@ -79,9 +79,9 @@ export default function ManageTokenPopup({ const searchTerm = event.target.value.toLowerCase() const filteredTokens = coinConfig?.filter((token) => { return ( - token?.symbol?.toLowerCase().includes(searchTerm) || - token?.name?.toLowerCase().includes(searchTerm) || - token?.address?.toLowerCase().includes(searchTerm) + token?.symbol?.toLowerCase().includes(searchTerm.trim()) || + token?.name?.toLowerCase().includes(searchTerm.trim()) || + token?.address?.toLowerCase().includes(searchTerm.trim()) ) }) setConfig(filteredTokens) diff --git a/src/pages/Assets/Tokens/index.tsx b/src/pages/Assets/Tokens/index.tsx index 2086b4adba..a9e0956d85 100644 --- a/src/pages/Assets/Tokens/index.tsx +++ b/src/pages/Assets/Tokens/index.tsx @@ -109,7 +109,10 @@ function Tokens(props): ReactElement { const searchTerm = event.target.value.toLowerCase() setSearch(searchTerm) const filteredTokens = updatedListTokens?.filter((token) => { - return token?.name?.toLowerCase().includes(searchTerm) || token?.address?.toLowerCase().includes(searchTerm) + return ( + token?.name?.toLowerCase().includes(searchTerm.trim()) || + token?.address?.toLowerCase().includes(searchTerm.trim()) + ) }) setListToken(filteredTokens) } diff --git a/src/pages/Avanced/Custom Transaction/index.tsx b/src/pages/Avanced/Custom Transaction/index.tsx index 756a2a8999..5e987e8dad 100644 --- a/src/pages/Avanced/Custom Transaction/index.tsx +++ b/src/pages/Avanced/Custom Transaction/index.tsx @@ -85,7 +85,7 @@ function CustomTransaction(props): ReactElement {
- + ) } diff --git a/src/pages/SmartContract/ContractInteraction/Contract.tsx b/src/pages/SmartContract/ContractInteraction/Contract.tsx index 389f17c1c5..d0d4f61d0f 100644 --- a/src/pages/SmartContract/ContractInteraction/Contract.tsx +++ b/src/pages/SmartContract/ContractInteraction/Contract.tsx @@ -159,7 +159,7 @@ function Contract({ contractData }): ReactElement { diff --git a/src/types/transaction.d.ts b/src/types/transaction.d.ts index 349b4d9c92..6786d0fe7b 100644 --- a/src/types/transaction.d.ts +++ b/src/types/transaction.d.ts @@ -60,6 +60,7 @@ export interface ITransactionListItem { FinalAmount?: number Timestamp?: number DisplayType?: string + ContractAddress?: string } export interface ISignature { diff --git a/src/utils/transactionUtils.ts b/src/utils/transactionUtils.ts index e41bb51718..c9c04547e0 100644 --- a/src/utils/transactionUtils.ts +++ b/src/utils/transactionUtils.ts @@ -112,6 +112,7 @@ const makeTransactions = (list: ITransactionListItem[]): MTransactionListItem[] value: tx?.Amount?.toString(), }, displayType: tx?.DisplayType, + contractAddress: tx?.ContractAddress, }, }, } From 1a0c57efa55b2abdac79610590cea01d5c879cd2 Mon Sep 17 00:00:00 2001 From: HoangNDM6 Date: Thu, 22 Jun 2023 17:34:15 +0700 Subject: [PATCH 61/69] fix import cw-20 in transaction --- src/logic/notifications/notificationTypes.ts | 7 +++ src/pages/Assets/Tokens/ImportTokenPopup.tsx | 25 +++++++-- .../Transactions/History/Transaction.tsx | 38 +++++++++++-- src/pages/Transactions/Queue/Transaction.tsx | 38 +++++++++++-- src/pages/Transactions/Queue/index.tsx | 27 +++++----- .../Transactions/components/TxAmount.tsx | 10 +++- .../components/TxDetail/Message.tsx | 54 +++++++++++++++---- .../components/TxDetail/index.tsx | 45 ++++++++++++++-- 8 files changed, 203 insertions(+), 41 deletions(-) diff --git a/src/logic/notifications/notificationTypes.ts b/src/logic/notifications/notificationTypes.ts index 1edf244ccc..cfa33a5e86 100644 --- a/src/logic/notifications/notificationTypes.ts +++ b/src/logic/notifications/notificationTypes.ts @@ -64,6 +64,7 @@ enum NOTIFICATION_IDS { SOMETHING_WENT_WRONG, TX_REJECTED_MSG_SUCCESS, TX_DELETED_MSG_SUCCESS, + IMPORT_TOKEN_SUCCESS, } export const NOTIFICATIONS: Record = { @@ -275,4 +276,10 @@ export const NOTIFICATIONS: Record = { message: 'Duplicate safe information!', options: { variant: ERROR, preventDuplicate: false, autoHideDuration: shortDuration }, }, + + //IMPORT CW_20 + IMPORT_TOKEN_SUCCESS: { + message: 'Import token successfully!', + options: { variant: SUCCESS, persist: false, autoHideDuration: shortDuration }, + }, } diff --git a/src/pages/Assets/Tokens/ImportTokenPopup.tsx b/src/pages/Assets/Tokens/ImportTokenPopup.tsx index 786d15b714..c46bd75fa0 100644 --- a/src/pages/Assets/Tokens/ImportTokenPopup.tsx +++ b/src/pages/Assets/Tokens/ImportTokenPopup.tsx @@ -1,19 +1,21 @@ import { useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' +import ic_defIcon from 'src/assets/icons/aura.png' import { FilledButton, OutlinedNeutralButton } from 'src/components/Button' import Gap from 'src/components/Gap' import TextField from 'src/components/Input/TextField' import Loader from 'src/components/Loader' import { Popup } from 'src/components/Popup' import Header from 'src/components/Popup/Header' +import { NOTIFICATIONS, enhanceSnackbarForAction } from 'src/logic/notifications' +import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar' +import { fetchMSafe } from 'src/logic/safe/store/actions/fetchSafe' import { updateSafe } from 'src/logic/safe/store/actions/updateSafe' import { currentSafeWithNames } from 'src/logic/safe/store/selectors' +import { extractSafeAddress, extractSafeId } from 'src/routes/routes' import { getDetailToken } from 'src/services' import { isValidAddress } from 'src/utils/isValidAddress' import styled from 'styled-components' -import ic_defIcon from 'src/assets/icons/aura.png' -import { extractSafeAddress, extractSafeId } from 'src/routes/routes' -import { fetchMSafe } from 'src/logic/safe/store/actions/fetchSafe' const Wrap = styled.div` width: 480px; @@ -54,7 +56,14 @@ const defaultToken = { logoUri: ic_defIcon, } -const ImportTokenPopup = ({ open, onBack, onClose }) => { +type IImportTokenPopup = { + open: boolean + onBack: () => void + onClose: () => void + onImport?: () => void + addressContract?: any +} +const ImportTokenPopup = ({ open, onBack, onClose, addressContract, onImport }: IImportTokenPopup) => { const dispatch = useDispatch() const [token, setToken] = useState(defaultToken) const { coinConfig, address } = useSelector(currentSafeWithNames) @@ -62,6 +71,12 @@ const ImportTokenPopup = ({ open, onBack, onClose }) => { const safeAddress = extractSafeAddress() const safeId = extractSafeId() as number + useEffect(() => { + if (addressContract) { + setToken({ ...token, address: addressContract }) + } + }, []) + const getContractDetail = async () => { setIsVerifiedContract('loading') try { @@ -109,6 +124,8 @@ const ImportTokenPopup = ({ open, onBack, onClose }) => { }), ) dispatch(fetchMSafe(safeAddress, safeId)) + onImport && onImport() + dispatch(enqueueSnackbar(enhanceSnackbarForAction(NOTIFICATIONS.IMPORT_TOKEN_SUCCESS))) onClose() setToken(defaultToken) } diff --git a/src/pages/Transactions/History/Transaction.tsx b/src/pages/Transactions/History/Transaction.tsx index 71edf4d936..5af4f1e70d 100644 --- a/src/pages/Transactions/History/Transaction.tsx +++ b/src/pages/Transactions/History/Transaction.tsx @@ -1,5 +1,6 @@ import { AccordionDetails } from '@aura/safe-react-components' -import { useState } from 'react' +import { useEffect, useState } from 'react' +import { getDetailToken } from 'src/services' import { formatTimeInWords } from 'src/utils/date' import TxAmount from '../components/TxAmount' import TxDetail from '../components/TxDetail' @@ -8,16 +9,45 @@ import TxStatus from '../components/TxStatus' import TxTime from '../components/TxTime' import TxType from '../components/TxType' import { NoPaddingAccordion, StyledAccordionSummary, StyledTransaction } from '../styled' + +let defToken export default function Transaction({ transaction, notFirstTx, listTokens }) { const [txDetailLoaded, setTxDetailLoaded] = useState(false) + if (transaction.txInfo.contractAddress) { + defToken = listTokens.find((t) => t.address === transaction.txInfo.contractAddress) + } else { + defToken = listTokens.find( + (t) => + t.denom === transaction.txInfo.denom || + t.minCoinDenom === transaction.txInfo.denom || + t.cosmosDenom === transaction.txInfo.denom, + ) + } + const [token, setToken] = useState(defToken) - const token = listTokens.find( - (t) => t.cosmosDenom === transaction.txInfo.denom || t.denom === transaction.txInfo.denom, - ) + useEffect(() => { + setToken(defToken) + }, [listTokens]) + + useEffect(() => { + if (!token) { + getContractDetail() + } + }, [token]) if (!transaction) { return null } + + const getContractDetail = async () => { + try { + const { data } = await getDetailToken(transaction?.txInfo?.contractAddress) + setToken({ ...data, isNotExist: true, address: transaction.txInfo.contractAddress }) + } catch (error) { + console.log(error) + } + } + return ( t.address === transaction.txInfo.contractAddress) + } else { + defToken = listTokens.find( + (t) => + t.denom === transaction.txInfo.denom || + t.minCoinDenom === transaction.txInfo.denom || + t.cosmosDenom === transaction.txInfo.denom, + ) + } + const [token, setToken] = useState(defToken) + + useEffect(() => { + setToken(defToken) + }, [listTokens]) + + useEffect(() => { + if (!token) { + getContractDetail() + } + }, [token]) if (!transaction) { return null } - const token = listTokens.find( - (t) => t.cosmosDenom === transaction.txInfo.denom || t.denom === transaction.txInfo.denom, - ) + + const getContractDetail = async () => { + try { + const { data } = await getDetailToken(transaction?.txInfo?.contractAddress) + setToken({ ...data, isNotExist: true, address: transaction.txInfo.contractAddress }) + } catch (error) { + console.log(error) + } + } return ( {}, }) export default function QueueTransactions(): ReactElement { - const { nextQueueSeq, sequence: currentSequence } = useSelector(currentSafeWithNames) - const { count, isLoading, hasMore, next, transactions } = usePagedQueuedTransactions() + const { sequence: currentSequence, coinConfig } = useSelector(currentSafeWithNames) + const { count, isLoading, transactions } = usePagedQueuedTransactions() const [txId, setTxId] = useState('') const [action, setAction] = useState('') const [open, setOpen] = useState(false) @@ -46,7 +44,6 @@ export default function QueueTransactions(): ReactElement { const dispatch = useDispatch() const safeAddress = extractSafeAddress() const safeId = extractSafeId() as number - const listTokens: any = useSelector(extendedSafeTokensSelector) const queryParams = useQuery() const transactionId = queryParams.get('transactionId') @@ -111,7 +108,7 @@ export default function QueueTransactions(): ReactElement {

{`Queued - Transaction with sequence ${curSeq} needs to be executed first`}

) : null} - + ) : ( @@ -130,7 +127,7 @@ export default function QueueTransactions(): ReactElement {

{txs.map((tx, index) => ( - + ))} diff --git a/src/pages/Transactions/components/TxAmount.tsx b/src/pages/Transactions/components/TxAmount.tsx index 2f4f07b416..1e7b837b0d 100644 --- a/src/pages/Transactions/components/TxAmount.tsx +++ b/src/pages/Transactions/components/TxAmount.tsx @@ -1,4 +1,5 @@ import { getNativeCurrency } from 'src/config' +import ListIcon from 'src/layout/Sidebar/ListIcon' import { convertAmount } from 'src/utils' type TxAmountProps = { @@ -11,9 +12,14 @@ export default function TxAmount({ amount = 0, token }: TxAmountProps) {
{amount ? ( <> - native-url-icon + {token?.isNotExist ? ( + + ) : ( + native-url-icon + )} +

- {convertAmount(amount, false, token?.decimals)} {token?.symbol} + {convertAmount(amount, false, token?.decimals)} {token?.symbol ?? token?.coinDenom}

) : ( diff --git a/src/pages/Transactions/components/TxDetail/Message.tsx b/src/pages/Transactions/components/TxDetail/Message.tsx index e4642bce81..90669a341a 100644 --- a/src/pages/Transactions/components/TxDetail/Message.tsx +++ b/src/pages/Transactions/components/TxDetail/Message.tsx @@ -1,5 +1,6 @@ import { Fragment, useEffect, useState } from 'react' import AddressInfo from 'src/components/AddressInfo' +import { FilledButton } from 'src/components/Button' import { Message } from 'src/components/CustomTransactionMessage/SmallMsg' import StatusCard from 'src/components/StatusCard' import { MsgTypeUrl } from 'src/logic/providers/constants/constant' @@ -21,7 +22,18 @@ const StyledStatus = styled.div` padding: 0; } ` -export default function TxMsg({ tx, txDetail, token }) { + +const BtnImport = ({ onImport }) => { + return ( + <> + + Import + + + ) +} +export default function TxMsg({ tx, txDetail, token, onImport }) { + const isTokenNotExist = token?.isNotExist const type = tx.txInfo.typeUrl const amount = convertAmount(txDetail.txMessage[0]?.amount || 0, false, token?.decimals) const [msg, setMsg] = useState([]) @@ -50,7 +62,8 @@ export default function TxMsg({ tx, txDetail, token }) { Send{' '} - {convertAmount(JSON.parse(txDetail?.txMessage[0].contractArgs)?.amount || '0', false)} {token?.symbol} + {convertAmount(JSON.parse(txDetail?.txMessage[0].contractArgs)?.amount || '0', false)} {token?.symbol}{' '} + {isTokenNotExist ? : <>} {' '} to: @@ -91,7 +104,11 @@ export default function TxMsg({ tx, txDetail, token }) { return (
- Delegate {amount} to: + Delegate{' '} + + {amount} {isTokenNotExist ? : <>} + {' '} + to:
@@ -101,7 +118,11 @@ export default function TxMsg({ tx, txDetail, token }) { return (
- Undelegate {amount} from: + Undelegate{' '} + + {amount} {isTokenNotExist ? : <>} + {' '} + from: {txDetail.autoClaimAmount ? ( @@ -121,7 +142,7 @@ export default function TxMsg({ tx, txDetail, token }) { Send{' '} - {amount} {token?.symbol} + {amount} {token?.symbol} {isTokenNotExist ? : <>} {' '} to: @@ -136,7 +157,11 @@ export default function TxMsg({ tx, txDetail, token }) { return (
- Send total of {formatNativeToken(totalAmount)} to: + Send total of{' '} + + {formatNativeToken(totalAmount)} {isTokenNotExist ? : <>} + {' '} + to: {txDetail?.txMessage[0].outputs.map((recipient, index) => (
@@ -153,7 +178,11 @@ export default function TxMsg({ tx, txDetail, token }) { return (
- Redelegate {amount} from: + Redelegate{' '} + + {amount} {isTokenNotExist ? : <>} + {' '} + from: To: @@ -171,7 +200,10 @@ export default function TxMsg({ tx, txDetail, token }) {
Vote {voteMapping[txDetail?.txMessage[0]?.voteOption]} on Proposal{' '} - {`#${txDetail?.txMessage[0]?.proposalId}`}: + + {`#${txDetail?.txMessage[0]?.proposalId}`} {isTokenNotExist ? : <>} + + :

{txDetail?.extraDetails?.proposalDetail?.title}

Voting end date: @@ -194,7 +226,11 @@ export default function TxMsg({ tx, txDetail, token }) { {msg?.amount && ( - Amount: {formatNativeToken(msg?.amount || 0)} + Amount:{' '} + + {formatNativeToken(msg?.amount || 0)}{' '} + {isTokenNotExist ? : <>} + )} diff --git a/src/pages/Transactions/components/TxDetail/index.tsx b/src/pages/Transactions/components/TxDetail/index.tsx index afdb368490..fe61be947a 100644 --- a/src/pages/Transactions/components/TxDetail/index.tsx +++ b/src/pages/Transactions/components/TxDetail/index.tsx @@ -1,16 +1,42 @@ import { Loader } from '@aura/safe-react-components' +import { useState } from 'react' import { useSelector } from 'react-redux' -import { getExplorerInfo } from 'src/config' -import { grantedSelector } from 'src/utils/safeUtils/selector' +import { getExplorerInfo, getInternalChainId } from 'src/config' +import ImportTokenPopup from 'src/pages/Assets/Tokens/ImportTokenPopup' +import { extractSafeAddress } from 'src/routes/routes' +import { getAllTx } from 'src/services' +import { DEFAULT_PAGE_SIZE } from 'src/services/constant/common' +import { ITransactionListQuery } from 'src/types/transaction' import { formatWithSchema } from 'src/utils/date' +import { grantedSelector } from 'src/utils/safeUtils/selector' import { useTransactionDetails } from '../../../../utils/hooks/useTransactionDetails' import { Centered, InlineEthHashInfo, TxDetailsContainer } from '../../styled' import { TxActions } from './Action' import TxMsg from './Message' import { TxOwners } from './Owner' + export default function TxDetail({ transaction, isHistoryTx, token }) { const isOwner = useSelector(grantedSelector) + const internalChainId = getInternalChainId() + const safeAddress = extractSafeAddress() const { data, loading } = useTransactionDetails(transaction.id, transaction.txHash, transaction.auraTxId) + const [importTokenPopup, setImportTokenPopup] = useState(false) + const address = token?.address + + const handleImport = () => { + setImportTokenPopup(true) + } + + const fetchTransactionsFromAuraApi = () => { + const payload: ITransactionListQuery = { + safeAddress, + pageIndex: 1, + pageSize: DEFAULT_PAGE_SIZE, + isHistory: false, + internalChainId: internalChainId, + } + getAllTx(payload) + } if (loading) { return ( @@ -58,7 +84,7 @@ export default function TxDetail({ transaction, isHistoryTx, token }) {

)}
- +
@@ -75,6 +101,19 @@ export default function TxDetail({ transaction, isHistoryTx, token }) { <> )}
+ {importTokenPopup && ( + { + setImportTokenPopup(false) + }} + onClose={() => { + setImportTokenPopup(false) + }} + onImport={fetchTransactionsFromAuraApi} + addressContract={address} + /> + )} ) } From f3648202ca49ed0e6ea008f788a9b6ba53cc7db7 Mon Sep 17 00:00:00 2001 From: HoangNDM6 Date: Fri, 23 Jun 2023 09:09:49 +0700 Subject: [PATCH 62/69] fix tx import token --- src/pages/Transactions/History/Transaction.tsx | 4 ++-- src/pages/Transactions/Queue/Transaction.tsx | 4 ++-- src/pages/Transactions/components/TxDetail/Message.tsx | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/pages/Transactions/History/Transaction.tsx b/src/pages/Transactions/History/Transaction.tsx index 5af4f1e70d..4f31754503 100644 --- a/src/pages/Transactions/History/Transaction.tsx +++ b/src/pages/Transactions/History/Transaction.tsx @@ -10,8 +10,8 @@ import TxTime from '../components/TxTime' import TxType from '../components/TxType' import { NoPaddingAccordion, StyledAccordionSummary, StyledTransaction } from '../styled' -let defToken export default function Transaction({ transaction, notFirstTx, listTokens }) { + let defToken const [txDetailLoaded, setTxDetailLoaded] = useState(false) if (transaction.txInfo.contractAddress) { defToken = listTokens.find((t) => t.address === transaction.txInfo.contractAddress) @@ -19,7 +19,7 @@ export default function Transaction({ transaction, notFirstTx, listTokens }) { defToken = listTokens.find( (t) => t.denom === transaction.txInfo.denom || - t.minCoinDenom === transaction.txInfo.denom || + t.symbol === transaction.txInfo.denom || t.cosmosDenom === transaction.txInfo.denom, ) } diff --git a/src/pages/Transactions/Queue/Transaction.tsx b/src/pages/Transactions/Queue/Transaction.tsx index 6b75ed5fab..e3586f38fa 100644 --- a/src/pages/Transactions/Queue/Transaction.tsx +++ b/src/pages/Transactions/Queue/Transaction.tsx @@ -12,7 +12,6 @@ import TxTime from '../components/TxTime' import TxType from '../components/TxType' import { NoPaddingAccordion, StyledAccordionSummary, StyledTransaction } from '../styled' -let defToken export default function Transaction({ transaction, hideSeq, @@ -25,13 +24,14 @@ export default function Transaction({ listTokens?: any }) { const [txDetailLoaded, setTxDetailLoaded] = useState(false) + let defToken if (transaction.txInfo.contractAddress) { defToken = listTokens.find((t) => t.address === transaction.txInfo.contractAddress) } else { defToken = listTokens.find( (t) => t.denom === transaction.txInfo.denom || - t.minCoinDenom === transaction.txInfo.denom || + t.symbol === transaction.txInfo.denom || t.cosmosDenom === transaction.txInfo.denom, ) } diff --git a/src/pages/Transactions/components/TxDetail/Message.tsx b/src/pages/Transactions/components/TxDetail/Message.tsx index 90669a341a..b05f5082c1 100644 --- a/src/pages/Transactions/components/TxDetail/Message.tsx +++ b/src/pages/Transactions/components/TxDetail/Message.tsx @@ -62,8 +62,8 @@ export default function TxMsg({ tx, txDetail, token, onImport }) { Send{' '} - {convertAmount(JSON.parse(txDetail?.txMessage[0].contractArgs)?.amount || '0', false)} {token?.symbol}{' '} - {isTokenNotExist ? : <>} + {convertAmount(JSON.parse(txDetail?.txMessage[0].contractArgs)?.amount || '0', false)}{' '} + {token?.symbol ?? token?.coinDenom} {isTokenNotExist ? : <>} {' '} to: @@ -142,7 +142,7 @@ export default function TxMsg({ tx, txDetail, token, onImport }) { Send{' '} - {amount} {token?.symbol} {isTokenNotExist ? : <>} + {amount} {token?.symbol ?? token?.coinDenom} {isTokenNotExist ? : <>} {' '} to: From 982034682f6bc0ccfc1b08766e5c1b9b1e55ffc4 Mon Sep 17 00:00:00 2001 From: imhson Date: Mon, 26 Jun 2023 09:48:32 +0700 Subject: [PATCH 63/69] fix token decimal at send cw20 tx --- src/pages/Transactions/components/TxDetail/Message.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/Transactions/components/TxDetail/Message.tsx b/src/pages/Transactions/components/TxDetail/Message.tsx index b05f5082c1..089b134a7d 100644 --- a/src/pages/Transactions/components/TxDetail/Message.tsx +++ b/src/pages/Transactions/components/TxDetail/Message.tsx @@ -62,7 +62,7 @@ export default function TxMsg({ tx, txDetail, token, onImport }) { Send{' '} - {convertAmount(JSON.parse(txDetail?.txMessage[0].contractArgs)?.amount || '0', false)}{' '} + {convertAmount(JSON.parse(txDetail?.txMessage[0].contractArgs)?.amount || '0', false, token?.decimals)}{' '} {token?.symbol ?? token?.coinDenom} {isTokenNotExist ? : <>} {' '} to: From d7e8ff5f238a6e705a1d0b3e0b29066d0402d264 Mon Sep 17 00:00:00 2001 From: imhson Date: Tue, 27 Jun 2023 11:11:44 +0700 Subject: [PATCH 64/69] get contract schema via indexer --- .../SafeListSidebar/AddSafeButton.tsx | 2 +- src/config/cache/chains.ts | 28 +++++++++++++++++- .../ContractInteraction/index.tsx | 29 +++++++++++-------- src/services/index.ts | 27 +++++++++++++++-- 4 files changed, 70 insertions(+), 16 deletions(-) diff --git a/src/components/SafeListSidebar/AddSafeButton.tsx b/src/components/SafeListSidebar/AddSafeButton.tsx index 8786c93115..65f19b3905 100644 --- a/src/components/SafeListSidebar/AddSafeButton.tsx +++ b/src/components/SafeListSidebar/AddSafeButton.tsx @@ -39,7 +39,7 @@ const AddSafeButton = ({ onAdd }: Props): ReactElement => { - Add Safe + Create new Safe diff --git a/src/config/cache/chains.ts b/src/config/cache/chains.ts index 3da6f6e6e4..da65c7871f 100644 --- a/src/config/cache/chains.ts +++ b/src/config/cache/chains.ts @@ -11,7 +11,33 @@ export const getChains = (): ChainInfo[] => chains export const loadChains = async () => { const networkList: ChainInfo[] = await getMChainsConfig(); - chains = networkList + chains = networkList.map((chain) => { + if (chain.chainId.includes('euphoria')) { + return { + ...chain, + environment: 'euphoria', + } + } + if (chain.chainId.includes('serenity')) { + return { + ...chain, + environment: 'serenity', + } + } + if (chain.chainId.includes('aura-testnet')) { + return { + ...chain, + environment: 'auratestnet', + } + } + if (chain.chainId.includes('xstaxy')) { + return { + ...chain, + environment: 'xstaxy', + } + } + return chain + }) // const { results = [] } = await getChainsConfig(GATEWAY_URL, { limit: 100 }) // chains = results // Set the initail web3 provider after loading chains diff --git a/src/pages/SmartContract/ContractInteraction/index.tsx b/src/pages/SmartContract/ContractInteraction/index.tsx index 90f8d16120..782382f3a3 100644 --- a/src/pages/SmartContract/ContractInteraction/index.tsx +++ b/src/pages/SmartContract/ContractInteraction/index.tsx @@ -1,20 +1,20 @@ +import { Validator } from 'jsonschema' import { ReactElement, useEffect, useState } from 'react' import Icon from 'src/assets/icons/FileText.svg' +import Alert from 'src/assets/icons/alert.svg' +import Check from 'src/assets/icons/check.svg' import Breadcrumb from 'src/components/Breadcrumb' import Gap from 'src/components/Gap' import TextArea from 'src/components/Input/TextArea' import TextField from 'src/components/Input/TextField' -import { getInternalChainId } from 'src/config' +import { makeSchemaInput } from 'src/components/JsonschemaForm/utils' +import Loader from 'src/components/Loader' +import Tooltip from 'src/components/Tooltip' import { getContract } from 'src/services' import { isValidAddress } from 'src/utils/isValidAddress' import styled from 'styled-components' import Contract from './Contract' -import Check from 'src/assets/icons/check.svg' -import Alert from 'src/assets/icons/alert.svg' -import Tooltip from 'src/components/Tooltip' -import { Validator } from 'jsonschema' -import { makeSchemaInput } from 'src/components/JsonschemaForm/utils' -import Loader from 'src/components/Loader' +import { getChainInfo } from 'src/config' const Wrap = styled.div` background: #24262e; border-radius: 8px; @@ -29,7 +29,6 @@ const Wrap = styled.div` ` function ContractInteraction(props): ReactElement { - const internalChainId = getInternalChainId() const [contractAddress, setContractAddress] = useState('') const [abi, setAbi] = useState('') const [contractData, setContractData] = useState({}) @@ -38,10 +37,16 @@ function ContractInteraction(props): ReactElement { const getContractDetail = async () => { setIsVerifiedContract('loading') - const { Data } = await getContract(contractAddress, internalChainId) - if (Data && (isValidAbi == 'false' || isValidAbi == null)) { - setContractData(Data) - setIsVerifiedContract(Data.verification ? 'true' : 'false') + const chainInfo = getChainInfo() as any + const { data } = await getContract(contractAddress) + const cData = data[chainInfo.environment].smart_contract[0].code.code_id_verifications[0] + const verification = cData.verification_status == 'SUCCESS' + if (cData && (isValidAbi == 'false' || isValidAbi == null)) { + setContractData({ + contractAddress: contractAddress, + executeMsgSchema: cData.execute_msg_schema, + }) + setIsVerifiedContract(verification ? 'true' : 'false') } else { setContractData({ contractAddress: contractAddress, diff --git a/src/services/index.ts b/src/services/index.ts index 0ff0e89e83..c7e80906cb 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -10,6 +10,7 @@ import { ICreateSafeTransaction, ITransactionListItem, ITransactionListQuery } f import { IMSafeInfo, IMSafeResponse, OwnedMSafes } from '../types/safe' let baseUrl = '' +let baseIndexerUrl = 'https://indexer-v2.dev.aurascan.io/api/v1/graphiql' let githubPageTokenRegistryUrl = '' let env = 'development' @@ -280,8 +281,30 @@ export async function getProposalDetail( ): Promise> { return axios.get(`${baseUrl}/gov/${internalChainId}/proposals/${proposalId}`).then((res) => res.data) } -export async function getContract(contractAddress: string, internalChainId: any): Promise> { - return axios.get(`${baseUrl}/contract/${contractAddress}?internalChainId=${internalChainId}`).then((res) => res.data) +export async function getContract(contractAddress: string): Promise> { + const chainInfo = getChainInfo() as any + return axios + .post(baseIndexerUrl, { + query: `query GetContractVerificationStatus($address: String = "") { + ${chainInfo.environment || ''} { + smart_contract(where: {address: {_eq: $address}}) { + code { + code_id_verifications { + compiler_version + verified_at + verification_status + execute_msg_schema + } + } + } + } + }`, + variables: { + address: contractAddress, + }, + operationName: 'GetContractVerificationStatus', + }) + .then((res) => res.data) } export async function getTokenDetail() { From 9bcc3d541dbafc27c78fbcc1a1142e1339e858b5 Mon Sep 17 00:00:00 2001 From: imhson Date: Tue, 27 Jun 2023 11:14:54 +0700 Subject: [PATCH 65/69] add indexer config --- src/config/cache/chains.ts | 4 ++++ src/services/index.ts | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/config/cache/chains.ts b/src/config/cache/chains.ts index da65c7871f..83168bc43e 100644 --- a/src/config/cache/chains.ts +++ b/src/config/cache/chains.ts @@ -15,24 +15,28 @@ export const loadChains = async () => { if (chain.chainId.includes('euphoria')) { return { ...chain, + indexerUrl: 'https://indexer-v2.dev.aurascan.io/api/v1/graphiql', environment: 'euphoria', } } if (chain.chainId.includes('serenity')) { return { ...chain, + indexerUrl: 'https://indexer-v2.dev.aurascan.io/api/v1/graphiql', environment: 'serenity', } } if (chain.chainId.includes('aura-testnet')) { return { ...chain, + indexerUrl: 'https://indexer-v2.dev.aurascan.io/api/v1/graphiql', environment: 'auratestnet', } } if (chain.chainId.includes('xstaxy')) { return { ...chain, + indexerUrl: 'https://indexer-v2.dev.aurascan.io/api/v1/graphiql', environment: 'xstaxy', } } diff --git a/src/services/index.ts b/src/services/index.ts index c7e80906cb..8a60f0e8be 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -284,7 +284,7 @@ export async function getProposalDetail( export async function getContract(contractAddress: string): Promise> { const chainInfo = getChainInfo() as any return axios - .post(baseIndexerUrl, { + .post(chainInfo.indexerurl, { query: `query GetContractVerificationStatus($address: String = "") { ${chainInfo.environment || ''} { smart_contract(where: {address: {_eq: $address}}) { From ff8a6885c4019dee5bc87ebc90fbead43941d4cf Mon Sep 17 00:00:00 2001 From: imhson Date: Tue, 27 Jun 2023 14:15:23 +0700 Subject: [PATCH 66/69] fix undefined --- src/pages/SmartContract/ContractInteraction/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/SmartContract/ContractInteraction/index.tsx b/src/pages/SmartContract/ContractInteraction/index.tsx index 782382f3a3..853b3b5aea 100644 --- a/src/pages/SmartContract/ContractInteraction/index.tsx +++ b/src/pages/SmartContract/ContractInteraction/index.tsx @@ -39,8 +39,8 @@ function ContractInteraction(props): ReactElement { setIsVerifiedContract('loading') const chainInfo = getChainInfo() as any const { data } = await getContract(contractAddress) - const cData = data[chainInfo.environment].smart_contract[0].code.code_id_verifications[0] - const verification = cData.verification_status == 'SUCCESS' + const cData = data[chainInfo.environment]?.smart_contract[0]?.code?.code_id_verifications[0] + const verification = cData?.verification_status == 'SUCCESS' if (cData && (isValidAbi == 'false' || isValidAbi == null)) { setContractData({ contractAddress: contractAddress, From 79f684660dbb4b0ab576c075dcdded6e1dc6eb64 Mon Sep 17 00:00:00 2001 From: imhson Date: Tue, 27 Jun 2023 14:47:18 +0700 Subject: [PATCH 67/69] fix --- src/services/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/index.ts b/src/services/index.ts index 8a60f0e8be..605a2aef8c 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -284,7 +284,7 @@ export async function getProposalDetail( export async function getContract(contractAddress: string): Promise> { const chainInfo = getChainInfo() as any return axios - .post(chainInfo.indexerurl, { + .post(chainInfo.indexerUrl, { query: `query GetContractVerificationStatus($address: String = "") { ${chainInfo.environment || ''} { smart_contract(where: {address: {_eq: $address}}) { From 6c3722cb6c6c13808bb4b853ac67097b9e50afa1 Mon Sep 17 00:00:00 2001 From: HoangNDM6 Date: Fri, 30 Jun 2023 15:53:20 +0700 Subject: [PATCH 68/69] fix show transaction funds --- src/components/JsonschemaForm/FundForm.tsx | 2 +- .../ContractInteraction/Contract.tsx | 35 ++++++++++--------- .../components/TxDetail/Message.tsx | 20 ++++++++++- 3 files changed, 39 insertions(+), 18 deletions(-) diff --git a/src/components/JsonschemaForm/FundForm.tsx b/src/components/JsonschemaForm/FundForm.tsx index b90ca226c7..63a575699a 100644 --- a/src/components/JsonschemaForm/FundForm.tsx +++ b/src/components/JsonschemaForm/FundForm.tsx @@ -70,7 +70,7 @@ const FundForm = ({ fund, onDelete, onChangeAmount }: IFundFormProps) => { useEffect(() => { setAmountValidateMsg('') - const tokenbalance = token?.balance.tokenBalance + const tokenbalance = fund.balance if (tokenbalance && +amount > +tokenbalance) { setAmountValidateMsg('Insufficient funds.') } diff --git a/src/pages/SmartContract/ContractInteraction/Contract.tsx b/src/pages/SmartContract/ContractInteraction/Contract.tsx index d0d4f61d0f..9d46ac2f20 100644 --- a/src/pages/SmartContract/ContractInteraction/Contract.tsx +++ b/src/pages/SmartContract/ContractInteraction/Contract.tsx @@ -6,16 +6,16 @@ import JsonschemaForm from 'src/components/JsonschemaForm' import { IFund } from 'src/components/JsonschemaForm/FundForm' import { makeSchemaInput } from 'src/components/JsonschemaForm/utils' import Loader from 'src/components/Loader' +import { addToFunds } from 'src/logic/contracts/store/actions' import { enhanceSnackbarForAction } from 'src/logic/notifications' import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar' import { MsgTypeUrl } from 'src/logic/providers/constants/constant' +import { Token } from 'src/logic/tokens/store/model/token' import { extractPrefixedSafeAddress, extractSafeAddress } from 'src/routes/routes' import { simulate } from 'src/services' +import { extendedSafeTokensSelector } from 'src/utils/safeUtils/selector' import styled from 'styled-components' import ReviewPopup from './ReviewPopup' -import { addToFunds } from 'src/logic/contracts/store/actions' -import { extendedSafeTokensSelector } from 'src/utils/safeUtils/selector' -import { Token } from 'src/logic/tokens/store/model/token' const Wrap = styled.div` .preview-button { @@ -42,19 +42,22 @@ function Contract({ contractData }): ReactElement { const [loading, setLoading] = useState(false) const [invalidAmount, setInvalidAmount] = useState(false) const tokenList = useSelector(extendedSafeTokensSelector) as unknown as Token[] - const defListTokens = tokenList.map((token) => ({ - id: token.denom, - denom: token.denom, - amount: '', - tokenDecimal: token.decimals, - logoUri: token.logoUri, - type: token.type, - symbol: token.symbol, - name: token.name, - balance: token.balance.tokenBalance, - address: token.address, - enabled: false, - })) as IFund[] + + const defListTokens = tokenList + .filter((t) => t.type !== 'CW20') + .map((token) => ({ + id: token.denom, + denom: token.cosmosDenom ?? token.denom, + amount: '', + tokenDecimal: token.decimals, + logoUri: token.logoUri, + type: token.type, + symbol: token.symbol, + name: token.name, + balance: token.balance.tokenBalance, + address: token.address, + enabled: false, + })) as IFund[] const preview = async () => { try { diff --git a/src/pages/Transactions/components/TxDetail/Message.tsx b/src/pages/Transactions/components/TxDetail/Message.tsx index 089b134a7d..027d4aa8a6 100644 --- a/src/pages/Transactions/components/TxDetail/Message.tsx +++ b/src/pages/Transactions/components/TxDetail/Message.tsx @@ -1,11 +1,14 @@ import { Fragment, useEffect, useState } from 'react' +import { useSelector } from 'react-redux' import AddressInfo from 'src/components/AddressInfo' import { FilledButton } from 'src/components/Button' import { Message } from 'src/components/CustomTransactionMessage/SmallMsg' import StatusCard from 'src/components/StatusCard' import { MsgTypeUrl } from 'src/logic/providers/constants/constant' +import { Token } from 'src/logic/tokens/store/model/token' import { beutifyJson, convertAmount, formatNativeToken } from 'src/utils' import { formatWithSchema } from 'src/utils/date' +import { extendedSafeTokensSelector } from 'src/utils/safeUtils/selector' import styled from 'styled-components' const voteMapping = { @@ -36,7 +39,9 @@ export default function TxMsg({ tx, txDetail, token, onImport }) { const isTokenNotExist = token?.isNotExist const type = tx.txInfo.typeUrl const amount = convertAmount(txDetail.txMessage[0]?.amount || 0, false, token?.decimals) - const [msg, setMsg] = useState([]) + const [msg, setMsg] = useState([]) + const tokenList = useSelector(extendedSafeTokensSelector) as unknown as Token[] + useEffect(() => { if (txDetail?.rawMessage) { setMsg(JSON.parse(txDetail?.rawMessage)) @@ -97,6 +102,19 @@ export default function TxMsg({ tx, txDetail, token, onImport }) {
))} +
Transaction funds:
+ {msg[0]?.value?.funds?.map((fund, index) => { + const foundToken = tokenList.find((token) => token.cosmosDenom === fund.denom || token.denom === fund.denom) + if (foundToken) { + return ( +
+

+ {convertAmount(fund.amount, false, +foundToken?.decimals)} {foundToken.symbol} +

+
+ ) + } + })}
) } From 5e542970ae2f030c6da76ef1c9ad9a0c340f1e35 Mon Sep 17 00:00:00 2001 From: HoangNDM6 Date: Fri, 30 Jun 2023 16:15:03 +0700 Subject: [PATCH 69/69] fix nativebalance --- src/components/SafeListSidebar/SafeList/SafeListItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/SafeListSidebar/SafeList/SafeListItem.tsx b/src/components/SafeListSidebar/SafeList/SafeListItem.tsx index 93529942bb..a43959221a 100644 --- a/src/components/SafeListSidebar/SafeList/SafeListItem.tsx +++ b/src/components/SafeListSidebar/SafeList/SafeListItem.tsx @@ -260,7 +260,7 @@ const SafeListItem = ({ {nativeBalance ? ( - {formatAmount(nativeBalance)} {nativeCurrencySymbol} + {+nativeBalance > 0 ? formatAmount(nativeBalance) : 0} {nativeCurrencySymbol} ) : showAddSafeLink ? (