Merge with dev

This commit is contained in:
Mati Dastugue 2020-08-11 13:35:53 -03:00
commit 4666272cdd
39 changed files with 1434 additions and 531 deletions

View File

@ -44,8 +44,20 @@ jobs:
uses: actions/setup-node@v1
with:
node-version: 10.16
- run: yarn install --network-concurrency 1
- run: |
mkdir .yarncache
yarn install --frozen-lockfile --cache-folder ./.yarncache
- name: Remove and cache clean (Windows Only)
if: startsWith(matrix.os, 'windows')
shell: powershell
run: |
rm -Recurse -Force .yarncache
yarn cache clean
- name: Remove and cache clean
if: "!startsWith(matrix.os, 'windows')"
run: |
rm -rf .yarncache
yarn cache clean
- name: Build/Release Desktop App
env:
# macOS notarization API key

View File

@ -85,6 +85,10 @@ const isSafeMethod = (methodId: string): boolean => {
}
export const decodeMethods = (data: string): DataDecoded | null => {
if(!data.length) {
return null
}
const [methodId, params] = [data.slice(0, 10), data.slice(10)]
if (isSafeMethod(methodId)) {

View File

@ -3,7 +3,7 @@ import createDecorator from 'final-form-calculate'
import React from 'react'
import { useField, useFormState } from 'react-final-form'
import { SafeApp } from 'src/routes/safe/components/Apps/types'
import { SafeApp } from 'src/routes/safe/components/Apps/types.d'
import { getAppInfoFromUrl, getIpfsLinkFromEns, uniqueApp } from 'src/routes/safe/components/Apps/utils'
import { composeValidators, required } from 'src/components/forms/validator'
import Field from 'src/components/forms/Field'

View File

@ -1,7 +1,7 @@
import React from 'react'
import { useFormState } from 'react-final-form'
import { SafeApp } from 'src/routes/safe/components/Apps/types'
import { SafeApp } from 'src/routes/safe/components/Apps/types.d'
import { isAppManifestValid } from 'src/routes/safe/components/Apps/utils'
interface SubmitButtonStatusProps {

View File

@ -6,7 +6,7 @@ import AppAgreement from './AppAgreement'
import AppUrl, { AppInfoUpdater, appUrlResolver } from './AppUrl'
import SubmitButtonStatus from './SubmitButtonStatus'
import { SafeApp } from 'src/routes/safe/components/Apps/types'
import { SafeApp } from 'src/routes/safe/components/Apps/types.d'
import GnoForm from 'src/components/forms/GnoForm'
import Img from 'src/components/layout/Img'
import appsIconSvg from 'src/routes/safe/components/Transactions/TxsTable/TxType/assets/appsIcon.svg'

View File

@ -0,0 +1,34 @@
import React from 'react'
import { FixedDialog, Text } from '@gnosis.pm/safe-react-components'
interface OwnProps {
onCancel: () => void
onConfirm: () => void
}
const LegalDisclaimer = ({ onCancel, onConfirm }: OwnProps): JSX.Element => (
<FixedDialog
body={
<>
<Text size="md">
You are now accessing third-party apps, which we do not own, control, maintain or audit. We are not liable for
any loss you may suffer in connection with interacting with the apps, which is at your own risk. You must read
our Terms, which contain more detailed provisions binding on you relating to the apps.
</Text>
<br />
<Text size="md">
I have read and understood the{' '}
<a href="https://gnosis-safe.io/terms" rel="noopener noreferrer" target="_blank">
Terms
</a>{' '}
and this Disclaimer, and agree to be bound by them.
</Text>
</>
}
onCancel={onCancel}
onConfirm={onConfirm}
title="Disclaimer"
/>
)
export default LegalDisclaimer

View File

@ -2,8 +2,8 @@ import { ButtonLink, ManageListModal } from '@gnosis.pm/safe-react-components'
import React, { useState } from 'react'
import appsIconSvg from 'src/routes/safe/components/Transactions/TxsTable/TxType/assets/appsIcon.svg'
import AddAppForm from './AddAppForm'
import { SafeApp } from './types'
import AddAppForm from '../AddAppForm'
import { SafeApp } from '../types'
const FORM_ID = 'add-apps-form'

View File

@ -75,7 +75,7 @@ const confirmTransactions = (
ethBalance: string,
nameApp: string,
iconApp: string,
txs: Array<SafeAppTx>,
txs: SafeAppTx[],
openModal: (modalInfo: GenericModalProps) => void,
closeModal: () => void,
onConfirm: () => void,

View File

@ -0,0 +1,107 @@
import { useState, useEffect, useCallback } from 'react'
import { loadFromStorage, saveToStorage } from 'src/utils/storage'
import { getAppInfoFromUrl, staticAppsList } from '../utils'
import { SafeApp, StoredSafeApp } from '../types'
const APPS_STORAGE_KEY = 'APPS_STORAGE_KEY'
type onAppToggleHandler = (appId: string, enabled: boolean) => Promise<void>
type onAppAddedHandler = (app: SafeApp) => void
type UseAppListReturnType = {
appList: SafeApp[]
loadingAppList: boolean
onAppToggle: onAppToggleHandler
onAppAdded: onAppAddedHandler
}
const useAppList = (): UseAppListReturnType => {
const [appList, setAppList] = useState<SafeApp[]>([])
const [loadingAppList, setLoadingAppList] = useState<boolean>(true)
// Load apps list
useEffect(() => {
const loadApps = async () => {
// recover apps from storage:
// * third-party apps added by the user
// * disabled status for both static and third-party apps
const persistedAppList = (await loadFromStorage<StoredSafeApp[]>(APPS_STORAGE_KEY)) || []
const list = [...persistedAppList]
staticAppsList.forEach((staticApp) => {
if (!list.some((persistedApp) => persistedApp.url === staticApp.url)) {
list.push(staticApp)
}
})
const apps = []
// using the appURL to recover app info
for (let index = 0; index < list.length; index++) {
try {
const currentApp = list[index]
const appInfo: any = await getAppInfoFromUrl(currentApp.url)
if (appInfo.error) {
throw Error(`There was a problem trying to load app ${currentApp.url}`)
}
appInfo.disabled = currentApp.disabled === undefined ? false : currentApp.disabled
apps.push(appInfo)
} catch (error) {
console.error(error)
}
}
setAppList(apps)
setLoadingAppList(false)
}
loadApps()
}, [])
const onAppToggle: onAppToggleHandler = useCallback(
async (appId, enabled) => {
// update in-memory list
const appListCopy = [...appList]
const app = appListCopy.find((a) => a.id === appId)
if (!app) {
return
}
app.disabled = !enabled
setAppList(appListCopy)
// update storage list
const listToPersist: StoredSafeApp[] = appListCopy.map(({ url, disabled }) => ({ url, disabled }))
saveToStorage(APPS_STORAGE_KEY, listToPersist)
},
[appList],
)
const onAppAdded: onAppAddedHandler = useCallback(
(app) => {
const newAppList = [
{ url: app.url, disabled: false },
...appList.map((a) => ({
url: a.url,
disabled: a.disabled,
})),
]
saveToStorage(APPS_STORAGE_KEY, newAppList)
setAppList([...appList, { ...app, disabled: false }])
},
[appList],
)
return {
appList,
loadingAppList,
onAppToggle,
onAppAdded,
}
}
export { useAppList }

View File

@ -0,0 +1,52 @@
import { useEffect } from 'react'
const useIframeMessageHandler = (): void => {
useEffect(() => {
// const handleIframeMessage = (data) => {
// if (!data || !data.messageId) {
// console.error('ThirdPartyApp: A message was received without message id.')
// return
// }
// switch (data.messageId) {
// case operations.SEND_TRANSACTIONS: {
// const onConfirm = async () => {
// closeModal()
// await sendTransactions(dispatch, safeAddress, data.data, enqueueSnackbar, closeSnackbar, selectedApp.id)
// }
// confirmTransactions(
// safeAddress,
// safeName,
// ethBalance,
// selectedApp.name,
// selectedApp.iconUrl,
// data.data,
// openModal,
// closeModal,
// onConfirm,
// )
// break
// }
// default: {
// console.error(`ThirdPartyApp: A message was received with an unknown message id ${data.messageId}.`)
// break
// }
// }
// }
// const onIframeMessage = async ({ data, origin }) => {
// if (origin === window.origin) {
// return
// }
// if (!selectedApp.url.includes(origin)) {
// console.error(`ThirdPartyApp: A message was received from an unknown origin ${origin}`)
// return
// }
// handleIframeMessage(data)
// }
// window.addEventListener('message', onIframeMessage)
// return () => {
// window.removeEventListener('message', onIframeMessage)
// }
}, [])
}
export { useIframeMessageHandler }

View File

@ -0,0 +1,29 @@
import { useState, useEffect, useCallback } from 'react'
import { loadFromStorage, saveToStorage } from 'src/utils/storage'
const APPS_LEGAL_CONSENT_RECEIVED = 'APPS_LEGAL_CONSENT_RECEIVED'
const useLegalConsent = (): { consentReceived: boolean; onConsentReceipt: () => void } => {
const [consentReceived, setConsentReceived] = useState<boolean>(false)
useEffect(() => {
const checkLegalDisclaimer = async () => {
const storedConsentReceived = await loadFromStorage(APPS_LEGAL_CONSENT_RECEIVED)
if (storedConsentReceived) {
setConsentReceived(true)
}
}
checkLegalDisclaimer()
}, [])
const onConsentReceipt = useCallback((): void => {
setConsentReceived(true)
saveToStorage(APPS_LEGAL_CONSENT_RECEIVED, true)
}, [])
return { consentReceived, onConsentReceipt }
}
export { useLegalConsent }

View File

@ -1,14 +1,16 @@
import { Card, FixedDialog, FixedIcon, IconText, Loader, Menu, Text, Title } from '@gnosis.pm/safe-react-components'
import { Card, FixedIcon, IconText, Loader, Menu, Title } from '@gnosis.pm/safe-react-components'
import { withSnackbar } from 'notistack'
import React, { useCallback, useEffect, useState } from 'react'
import React, { useCallback, useEffect, useState, useMemo } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { useHistory } from 'react-router-dom'
import styled from 'styled-components'
import ManageApps from './ManageApps'
import ManageApps from './components/ManageApps'
import confirmTransactions from './confirmTransactions'
import sendTransactions from './sendTransactions'
import { getAppInfoFromUrl, staticAppsList } from './utils'
import LegalDisclaimer from './components/LegalDisclaimer'
import { useLegalConsent } from './hooks/useLegalConsent'
import { useAppList } from './hooks/useAppList'
import LCL from 'src/components/ListContentLayout'
import { networkSelector } from 'src/logic/wallets/store/selectors'
@ -19,12 +21,7 @@ import {
safeNameSelector,
safeParamAddressFromStateSelector,
} from 'src/routes/safe/store/selectors'
import { loadFromStorage, saveToStorage } from 'src/utils/storage'
import { isSameHref } from 'src/utils/url'
import { SafeApp, StoredSafeApp } from './types'
const APPS_STORAGE_KEY = 'APPS_STORAGE_KEY'
const APPS_LEGAL_DISCLAIMER_STORAGE_KEY = 'APPS_LEGAL_DISCLAIMER_STORAGE_KEY'
const StyledIframe = styled.iframe`
padding: 15px;
@ -63,11 +60,10 @@ const operations = {
}
function Apps({ closeModal, closeSnackbar, enqueueSnackbar, openModal }) {
const [appList, setAppList] = useState<Array<SafeApp>>([])
const [legalDisclaimerAccepted, setLegalDisclaimerAccepted] = useState(false)
const [selectedApp, setSelectedApp] = useState<string>()
const [loading, setLoading] = useState(true)
const [appIsLoading, setAppIsLoading] = useState(true)
const { appList, loadingAppList, onAppToggle, onAppAdded } = useAppList()
const [appIsLoading, setAppIsLoading] = useState<boolean>(true)
const [selectedAppId, setSelectedAppId] = useState<string>()
const [iframeEl, setIframeEl] = useState<HTMLIFrameElement | null>(null)
const history = useHistory()
@ -77,8 +73,36 @@ function Apps({ closeModal, closeSnackbar, enqueueSnackbar, openModal }) {
const network = useSelector(networkSelector)
const ethBalance = useSelector(safeEthBalanceSelector)
const dispatch = useDispatch()
const { consentReceived, onConsentReceipt } = useLegalConsent()
const getSelectedApp = useCallback(() => appList.find((e) => e.id === selectedApp), [appList, selectedApp])
const selectedApp = useMemo(() => appList.find((app) => app.id === selectedAppId), [appList, selectedAppId])
const onSelectApp = useCallback(
(appId) => {
if (selectedAppId === appId) {
return
}
setAppIsLoading(true)
setSelectedAppId(appId)
},
[selectedAppId],
)
useEffect(() => {
const selectFirstEnabledApp = () => {
const firstEnabledApp = appList.find((a) => !a.disabled)
if (firstEnabledApp) {
setSelectedAppId(firstEnabledApp.id)
}
}
const initialSelect = appList.length && !selectedAppId
const currentAppWasDisabled = selectedApp?.disabled
if (initialSelect || currentAppWasDisabled) {
selectFirstEnabledApp()
}
}, [appList, selectedApp, selectedAppId])
const iframeRef = useCallback((node) => {
if (node !== null) {
@ -86,58 +110,15 @@ function Apps({ closeModal, closeSnackbar, enqueueSnackbar, openModal }) {
}
}, [])
const onSelectApp = useCallback(
(appId) => {
const selectedApp = getSelectedApp()
if (selectedApp && selectedApp.id === appId) {
return
}
setAppIsLoading(true)
setSelectedApp(appId)
},
[getSelectedApp],
)
const redirectToBalance = () => history.push(`${SAFELIST_ADDRESS}/${safeAddress}/balances`)
const onAcceptLegalDisclaimer = () => {
setLegalDisclaimerAccepted(true)
saveToStorage(APPS_LEGAL_DISCLAIMER_STORAGE_KEY, true)
}
const getContent = () => {
if (!selectedApp) {
return null
}
if (!legalDisclaimerAccepted) {
return (
<FixedDialog
body={
<>
<Text size="md">
You are now accessing third-party apps, which we do not own, control, maintain or audit. We are not
liable for any loss you may suffer in connection with interacting with the apps, which is at your own
risk. You must read our Terms, which contain more detailed provisions binding on you relating to the
apps.
</Text>
<br />
<Text size="md">
I have read and understood the{' '}
<a href="https://gnosis-safe.io/terms" rel="noopener noreferrer" target="_blank">
Terms
</a>{' '}
and this Disclaimer, and agree to be bound by them.
</Text>
</>
}
onCancel={redirectToBalance}
onConfirm={onAcceptLegalDisclaimer}
title="Disclaimer"
/>
)
if (!consentReceived) {
return <LegalDisclaimer onCancel={redirectToBalance} onConfirm={onConsentReceipt} />
}
if (network === 'UNKNOWN' || !granted) {
@ -149,8 +130,6 @@ function Apps({ closeModal, closeSnackbar, enqueueSnackbar, openModal }) {
)
}
const app = getSelectedApp()
return (
<IframeWrapper>
{appIsLoading && (
@ -158,76 +137,26 @@ function Apps({ closeModal, closeSnackbar, enqueueSnackbar, openModal }) {
<Loader size="md" />
</LoadingContainer>
)}
<StyledIframe frameBorder="0" id={`iframe-${app.name}`} ref={iframeRef} src={app.url} title={app.name} />
<StyledIframe
frameBorder="0"
id={`iframe-${selectedApp.name}`}
ref={iframeRef}
src={selectedApp.url}
title={selectedApp.name}
/>
</IframeWrapper>
)
}
const onAppAdded = (app: SafeApp) => {
const newAppList = [
{ url: app.url, disabled: false },
...appList.map((a) => ({
url: a.url,
disabled: a.disabled,
})),
]
saveToStorage(APPS_STORAGE_KEY, newAppList)
setAppList([...appList, { ...app, disabled: false }])
}
const selectFirstApp = useCallback(
(apps) => {
const firstEnabledApp = apps.find((a) => !a.disabled)
if (firstEnabledApp) {
onSelectApp(firstEnabledApp.id)
}
},
[onSelectApp],
)
const onAppToggle = async (appId, enabled) => {
// update in-memory list
const copyAppList = [...appList]
const app = copyAppList.find((a) => a.id === appId)
if (!app) {
return
}
app.disabled = !enabled
setAppList(copyAppList)
// update storage list
const persistedAppList = (await loadFromStorage<StoredSafeApp[]>(APPS_STORAGE_KEY)) || []
let storageApp = persistedAppList.find((a) => a.url === app.url)
if (!storageApp) {
storageApp = { url: app.url }
storageApp.disabled = !enabled
persistedAppList.push(storageApp)
} else {
storageApp.disabled = !enabled
}
saveToStorage(APPS_STORAGE_KEY, persistedAppList)
// select app
const selectedApp = getSelectedApp()
if (!selectedApp || (selectedApp && selectedApp.id === appId)) {
setSelectedApp(undefined)
selectFirstApp(copyAppList)
}
}
const getEnabledApps = () => appList.filter((a) => !a.disabled)
const enabledApps = useMemo(() => appList.filter((a) => !a.disabled), [appList])
const sendMessageToIframe = useCallback(
(messageId, data) => {
const app = getSelectedApp()
iframeEl?.contentWindow.postMessage({ messageId, data }, app.url)
if (iframeEl && selectedApp) {
iframeEl.contentWindow.postMessage({ messageId, data }, selectedApp.url)
}
},
[getSelectedApp, iframeEl],
[iframeEl, selectedApp],
)
// handle messages from iframe
@ -242,22 +171,16 @@ function Apps({ closeModal, closeSnackbar, enqueueSnackbar, openModal }) {
case operations.SEND_TRANSACTIONS: {
const onConfirm = async () => {
closeModal()
await sendTransactions(
dispatch,
safeAddress,
data.data,
enqueueSnackbar,
closeSnackbar,
getSelectedApp().id,
)
await sendTransactions(dispatch, safeAddress, data.data, enqueueSnackbar, closeSnackbar, selectedApp.id)
}
confirmTransactions(
safeAddress,
safeName,
ethBalance,
getSelectedApp().name,
getSelectedApp().iconUrl,
selectedApp.name,
selectedApp.iconUrl,
data.data,
openModal,
closeModal,
@ -287,8 +210,7 @@ function Apps({ closeModal, closeSnackbar, enqueueSnackbar, openModal }) {
return
}
const app = getSelectedApp()
if (!app.url.includes(origin)) {
if (!selectedApp.url.includes(origin)) {
console.error(`ThirdPartyApp: A message was received from an unknown origin ${origin}`)
return
}
@ -303,65 +225,11 @@ function Apps({ closeModal, closeSnackbar, enqueueSnackbar, openModal }) {
}
})
// load legalDisclaimer
useEffect(() => {
const checkLegalDisclaimer = async () => {
const legalDisclaimer = await loadFromStorage(APPS_LEGAL_DISCLAIMER_STORAGE_KEY)
if (legalDisclaimer) {
setLegalDisclaimerAccepted(true)
}
}
checkLegalDisclaimer()
}, [])
// Load apps list
useEffect(() => {
const loadApps = async () => {
// recover apps from storage:
// * third-party apps added by the user
// * disabled status for both static and third-party apps
const persistedAppList = (await loadFromStorage<StoredSafeApp[]>(APPS_STORAGE_KEY)) || []
const list = [...persistedAppList]
staticAppsList.forEach((staticApp) => {
if (!list.some((persistedApp) => persistedApp.url === staticApp.url)) {
list.push(staticApp)
}
})
const apps = []
// using the appURL to recover app info
for (let index = 0; index < list.length; index++) {
try {
const currentApp = list[index]
const appInfo: SafeApp = await getAppInfoFromUrl(currentApp.url)
if (appInfo.error) {
throw Error(`There was a problem trying to load app ${currentApp.url}`)
}
appInfo.disabled = currentApp.disabled === undefined ? false : currentApp.disabled
apps.push(appInfo)
} catch (error) {
console.error(error)
}
}
setAppList(apps)
setLoading(false)
selectFirstApp(apps)
}
if (!appList.length) {
loadApps()
}
}, [appList.length, selectFirstApp])
// on iframe change
useEffect(() => {
const sendMessageToIframe = (messageId, data) => {
iframeEl.contentWindow.postMessage({ messageId, data }, selectedApp.url)
}
const onIframeLoaded = () => {
setAppIsLoading(false)
sendMessageToIframe(operations.ON_SAFE_INFO, {
@ -371,8 +239,7 @@ function Apps({ closeModal, closeSnackbar, enqueueSnackbar, openModal }) {
})
}
const app = getSelectedApp()
if (!iframeEl || !selectedApp || !isSameHref(iframeEl.src, app.url)) {
if (!iframeEl || !selectedApp || !isSameHref(iframeEl.src, selectedApp.url)) {
return
}
@ -381,9 +248,9 @@ function Apps({ closeModal, closeSnackbar, enqueueSnackbar, openModal }) {
return () => {
iframeEl.removeEventListener('load', onIframeLoaded)
}
}, [ethBalance, getSelectedApp, iframeEl, network, safeAddress, selectedApp, sendMessageToIframe])
}, [ethBalance, iframeEl, network, safeAddress, selectedApp])
if (loading || !appList.length) {
if (loadingAppList || !appList.length) {
return (
<LoadingContainer>
<Loader size="md" />
@ -396,26 +263,12 @@ function Apps({ closeModal, closeSnackbar, enqueueSnackbar, openModal }) {
<Menu>
<ManageApps appList={appList} onAppAdded={onAppAdded} onAppToggle={onAppToggle} />
</Menu>
{getEnabledApps().length ? (
{enabledApps.length ? (
<LCL.Wrapper>
<LCL.Menu>
<LCL.List activeItem={selectedApp} items={getEnabledApps()} onItemClick={onSelectApp} />
<LCL.List activeItem={selectedAppId} items={enabledApps} onItemClick={onSelectApp} />
</LCL.Menu>
<LCL.Content>{getContent()}</LCL.Content>
{/* <LCL.Footer>
{getSelectedApp() && getSelectedApp().providedBy && (
<>
<p>This App is provided by </p>
<ButtonLink
onClick={() => window.open(getSelectedApp().providedBy.url, '_blank')}
size="lg"
testId="manage-tokens-btn"
>
{selectedApp && getSelectedApp().providedBy.name}
</ButtonLink>
</>
)}
</LCL.Footer> */}
</LCL.Wrapper>
) : (
<Card style={{ marginBottom: '24px' }}>

View File

@ -4,7 +4,7 @@ import { List } from 'immutable'
import { FIXED } from 'src/components/Table/sorting'
import { formatAmountInUsFormat } from 'src/logic/tokens/utils/formatAmount'
import { ETH_ADDRESS } from 'src/logic/tokens/utils/tokenHelpers'
import { TableColumn } from 'src/components/Table/types'
import { TableColumn } from 'src/components/Table/types.d'
import { AVAILABLE_CURRENCIES, BalanceCurrencyList } from 'src/logic/currencyValues/store/model/currencyValues'
import { Token } from 'src/logic/tokens/store/model/token'

View File

@ -1,8 +1,8 @@
import Tab from '@material-ui/core/Tab'
import Tabs from '@material-ui/core/Tabs'
import { withStyles } from '@material-ui/core/styles'
import { makeStyles } from '@material-ui/core/styles'
import React from 'react'
import { withRouter, RouteComponentProps } from 'react-router-dom'
import { useRouteMatch, useLocation, useHistory } from 'react-router-dom'
import { styles } from './style'
@ -19,10 +19,6 @@ import { AppsIcon } from 'src/routes/safe/components/assets/AppsIcon'
import { BalancesIcon } from 'src/routes/safe/components/assets/BalancesIcon'
import { TransactionsIcon } from 'src/routes/safe/components/assets/TransactionsIcon'
interface Props extends RouteComponentProps {
classes: Record<string, any>
}
const BalancesLabel = (
<>
<BalancesIcon />
@ -51,12 +47,15 @@ const TransactionsLabel = (
</>
)
const TabsComponent = (props: Props) => {
const { classes, location, match } = props
const useStyles = makeStyles(styles)
const handleCallToRouter = (_, value) => {
const { history } = props
const TabsComponent = (): React.ReactElement => {
const classes = useStyles()
const match = useRouteMatch()
const location = useLocation()
const history = useHistory()
const handleCallToRouter = (_: unknown, value: string) => {
history.push(value)
}
@ -128,4 +127,4 @@ const TabsComponent = (props: Props) => {
</Tabs>
)
}
export default withStyles(styles as any)(withRouter(TabsComponent))
export default TabsComponent

View File

@ -1,6 +1,7 @@
import { secondary } from 'src/theme/variables'
import { createStyles } from '@material-ui/core'
export const styles = () => ({
export const styles = createStyles({
tabWrapper: {
display: 'flex',
flexDirection: 'row',

View File

@ -2,7 +2,7 @@ import { GenericModal } from '@gnosis.pm/safe-react-components'
import { makeStyles } from '@material-ui/core/styles'
import React, { useState } from 'react'
import { useSelector } from 'react-redux'
import { Redirect, Route, Switch, withRouter, RouteComponentProps } from 'react-router-dom'
import { Redirect, Route, Switch, useRouteMatch } from 'react-router-dom'
import Receive from '../Balances/Receive'
@ -32,7 +32,7 @@ const Balances = React.lazy(() => import('../Balances'))
const TxsTable = React.lazy(() => import('src/routes/safe/components/Transactions/TxsTable'))
const AddressBookTable = React.lazy(() => import('src/routes/safe/components/AddressBook'))
interface Props extends RouteComponentProps {
interface Props {
sendFunds: Record<string, any>
showReceive: boolean
onShow: (value: string) => void
@ -41,11 +41,12 @@ interface Props extends RouteComponentProps {
hideSendFunds: () => void
}
const useStyles = makeStyles(styles as any)
const useStyles = makeStyles(styles)
const Layout = (props: Props) => {
const Layout = (props: Props): React.ReactElement => {
const classes = useStyles()
const { hideSendFunds, match, onHide, onShow, sendFunds, showReceive, showSendFunds } = props
const { hideSendFunds, onHide, onShow, sendFunds, showReceive, showSendFunds } = props
const match = useRouteMatch()
const [modal, setModal] = useState({
isOpen: false,
@ -117,4 +118,4 @@ const Layout = (props: Props) => {
)
}
export default withRouter(Layout)
export default Layout

View File

@ -1,6 +1,7 @@
import { screenSm, sm } from 'src/theme/variables'
import { createStyles } from '@material-ui/core'
export const styles = () => ({
export const styles = createStyles({
receiveModal: {
height: 'auto',
maxWidth: 'calc(100% - 30px)',

View File

@ -1,5 +1,5 @@
import { List } from 'immutable'
import { TableColumn } from 'src/components/Table/types'
import { TableColumn } from 'src/components/Table/types.d'
import { ModulePair } from 'src/routes/safe/store/models/safe'
export const MODULES_TABLE_ADDRESS_ID = 'address'

View File

@ -7,19 +7,15 @@ import Paragraph from 'src/components/layout/Paragraph'
import { useWindowDimensions } from 'src/routes/safe/container/hooks/useWindowDimensions'
import { useEffect, useState } from 'react'
interface OwnerAddressTableCellProps {
type OwnerAddressTableCellProps = {
address: string
knownAddress?: boolean
showLinks: boolean
userName?: string
}
const OwnerAddressTableCell = ({
address,
knownAddress,
showLinks,
userName,
}: OwnerAddressTableCellProps): React.ReactElement => {
const OwnerAddressTableCell = (props: OwnerAddressTableCellProps): React.ReactElement => {
const { address, knownAddress, showLinks, userName } = props
const [cut, setCut] = useState(undefined)
const { width } = useWindowDimensions()
@ -38,7 +34,7 @@ const OwnerAddressTableCell = ({
<Identicon address={address} diameter={32} />
{showLinks ? (
<div style={{ marginLeft: 10, flexShrink: 1, minWidth: 0 }}>
{userName}
{!userName || userName === 'UNKNOWN' ? null : userName}
<EtherScanLink knownAddress={knownAddress} type="address" value={address} cut={cut} />
</div>
) : (

View File

@ -1,5 +1,5 @@
import { List } from 'immutable'
import { TableColumn } from 'src/components/Table/types'
import { TableColumn } from 'src/components/Table/types.d'
export const OWNERS_TABLE_NAME_ID = 'name'
export const OWNERS_TABLE_ADDRESS_ID = 'address'

View File

@ -1,4 +1,4 @@
import { withStyles } from '@material-ui/core/styles'
import { makeStyles } from '@material-ui/core/styles'
import cn from 'classnames'
import React from 'react'
import { useSelector } from 'react-redux'
@ -18,31 +18,55 @@ import Button from 'src/components/layout/Button'
import Img from 'src/components/layout/Img'
import Paragraph from 'src/components/layout/Paragraph'
import { getNameFromAddressBook } from 'src/logic/addressBook/store/selectors'
import { OwnersWithoutConfirmations } from './index'
export const CONFIRM_TX_BTN_TEST_ID = 'confirm-btn'
export const EXECUTE_TX_BTN_TEST_ID = 'execute-btn'
export const REJECT_TX_BTN_TEST_ID = 'reject-btn'
export const EXECUTE_REJECT_TX_BTN_TEST_ID = 'execute-reject-btn'
const OwnerComponent = ({
classes,
confirmed,
executor,
isCancelTx,
onTxConfirm,
onTxExecute,
onTxReject,
owner,
pendingAcceptAction,
pendingRejectAction,
showConfirmBtn,
showExecuteBtn,
showExecuteRejectBtn,
showRejectBtn,
thresholdReached,
userAddress,
}: any) => {
type OwnerComponentProps = {
executor: string
isCancelTx?: boolean
onTxConfirm?: () => void
onTxExecute?: () => void
onTxReject?: () => void
ownersUnconfirmed: OwnersWithoutConfirmations
ownersWhoConfirmed: string[]
showConfirmBtn?: boolean
showExecuteBtn?: boolean
showExecuteRejectBtn?: boolean
showRejectBtn?: boolean
thresholdReached: boolean
userAddress: string
confirmed?: boolean
owner: string
pendingAcceptAction?: boolean
pendingRejectAction?: boolean
}
const useStyles = makeStyles(styles)
const OwnerComponent = (props: OwnerComponentProps): React.ReactElement => {
const {
owner,
pendingAcceptAction,
pendingRejectAction,
isCancelTx,
thresholdReached,
executor,
showConfirmBtn,
onTxConfirm,
onTxExecute,
showExecuteBtn,
showRejectBtn,
userAddress,
onTxReject,
showExecuteRejectBtn,
confirmed,
} = props
const nameInAdbk = useSelector((state) => getNameFromAddressBook(state, owner))
const classes = useStyles()
const [imgCircle, setImgCircle] = React.useState(ConfirmSmallGreyCircle)
React.useMemo(() => {
@ -155,8 +179,8 @@ const OwnerComponent = ({
</div>
<Identicon address={owner} className={classes.icon} diameter={32} />
<Block>
<Paragraph className={classes.name} noMargin>
{nameInAdbk}
<Paragraph className={nameInAdbk === 'UNKNOWN' ? null : classes.name} noMargin>
{!nameInAdbk || nameInAdbk === 'UNKNOWN' ? null : nameInAdbk}
</Paragraph>
<EtherscanLink className={classes.address} cut={4} type="address" value={owner} />
</Block>
@ -167,4 +191,4 @@ const OwnerComponent = ({
)
}
export default withStyles(styles as any)(OwnerComponent)
export default OwnerComponent

View File

@ -1,64 +1,42 @@
import { withStyles } from '@material-ui/core/styles'
import React from 'react'
import OwnerComponent from './OwnerComponent'
import { styles } from './style'
import { OwnersWithoutConfirmations } from './index'
const OwnersList = ({
classes,
executor,
isCancelTx,
onTxConfirm,
onTxExecute,
onTxReject,
ownersUnconfirmed,
ownersWhoConfirmed,
showConfirmBtn,
showExecuteBtn,
showExecuteRejectBtn,
showRejectBtn,
thresholdReached,
userAddress,
}: any) => (
<>
{ownersWhoConfirmed.map((owner) => (
<OwnerComponent
classes={classes}
confirmed
executor={executor}
isCancelTx={isCancelTx}
key={owner}
onTxExecute={onTxExecute}
onTxReject={onTxReject}
owner={owner}
showExecuteBtn={showExecuteBtn}
showExecuteRejectBtn={showExecuteRejectBtn}
showRejectBtn={showRejectBtn}
thresholdReached={thresholdReached}
userAddress={userAddress}
/>
))}
{ownersUnconfirmed.map(({ hasPendingAcceptActions, hasPendingRejectActions, owner }) => (
<OwnerComponent
classes={classes}
executor={executor}
isCancelTx={isCancelTx}
key={owner}
onTxConfirm={onTxConfirm}
onTxExecute={onTxExecute}
onTxReject={onTxReject}
owner={owner}
pendingAcceptAction={hasPendingAcceptActions}
pendingRejectAction={hasPendingRejectActions}
showConfirmBtn={showConfirmBtn}
showExecuteBtn={showExecuteBtn}
showExecuteRejectBtn={showExecuteRejectBtn}
showRejectBtn={showRejectBtn}
thresholdReached={thresholdReached}
userAddress={userAddress}
/>
))}
</>
)
type OwnersListProps = {
executor: string
isCancelTx?: boolean
onTxConfirm?: () => void
onTxExecute?: () => void
onTxReject?: () => void
ownersUnconfirmed: OwnersWithoutConfirmations
ownersWhoConfirmed: string[]
showConfirmBtn?: boolean
showExecuteBtn?: boolean
showExecuteRejectBtn?: boolean
showRejectBtn?: boolean
thresholdReached: boolean
userAddress: string
}
export default withStyles(styles as any)(OwnersList)
const OwnersList = (props: OwnersListProps): React.ReactElement => {
const { ownersUnconfirmed, ownersWhoConfirmed } = props
return (
<>
{ownersWhoConfirmed.map((owner) => (
<OwnerComponent confirmed key={owner} owner={owner} {...props} />
))}
{ownersUnconfirmed.map(({ hasPendingAcceptActions, hasPendingRejectActions, owner }) => (
<OwnerComponent
key={owner}
owner={owner}
pendingAcceptAction={hasPendingAcceptActions}
pendingRejectAction={hasPendingRejectActions}
{...props}
/>
))}
</>
)
}
export default OwnersList

View File

@ -1,4 +1,3 @@
import { withStyles } from '@material-ui/core/styles'
import cn from 'classnames'
import React from 'react'
import { useSelector } from 'react-redux'
@ -9,7 +8,6 @@ import CheckLargeFilledRedCircle from './assets/check-large-filled-red.svg'
import ConfirmLargeGreenCircle from './assets/confirm-large-green.svg'
import ConfirmLargeGreyCircle from './assets/confirm-large-grey.svg'
import ConfirmLargeRedCircle from './assets/confirm-large-red.svg'
import { styles } from './style'
import Block from 'src/components/layout/Block'
import Col from 'src/components/layout/Col'
@ -18,10 +16,19 @@ import Paragraph from 'src/components/layout/Paragraph/index'
import { userAccountSelector } from 'src/logic/wallets/store/selectors'
import { makeTransaction } from 'src/routes/safe/store/models/transaction'
import { safeOwnersSelector, safeThresholdSelector } from 'src/routes/safe/store/selectors'
import { TransactionStatus } from 'src/routes/safe/store/models/types/transaction'
import { Transaction, TransactionStatus } from 'src/routes/safe/store/models/types/transaction'
import { List } from 'immutable'
import { makeStyles } from '@material-ui/core/styles'
import { styles } from './style'
function getOwnersConfirmations(tx, userAddress) {
const ownersWhoConfirmed = []
export type OwnersWithoutConfirmations = {
hasPendingAcceptActions: boolean
hasPendingRejectActions: boolean
owner: string
}[]
function getOwnersConfirmations(tx: Transaction, userAddress: string): [string[], boolean] {
const ownersWhoConfirmed: string[] = []
let currentUserAlreadyConfirmed = false
tx.confirmations.forEach((conf) => {
@ -34,7 +41,11 @@ function getOwnersConfirmations(tx, userAddress) {
return [ownersWhoConfirmed, currentUserAlreadyConfirmed]
}
function getPendingOwnersConfirmations(owners, tx, userAddress) {
function getPendingOwnersConfirmations(
owners: List<{ name: string; address: string }>,
tx: Transaction,
userAddress: string,
): [OwnersWithoutConfirmations, boolean] {
const ownersWithNoConfirmations = []
let currentUserNotConfirmed = true
@ -74,10 +85,23 @@ function getPendingOwnersConfirmations(owners, tx, userAddress) {
return [ownersWithNoConfirmationsSorted, currentUserNotConfirmed]
}
const useStyles = makeStyles(styles)
type ownersColumnProps = {
tx: Transaction
cancelTx: Transaction
thresholdReached: boolean
cancelThresholdReached: boolean
onTxConfirm: () => void
onTxExecute: () => void
onTxReject: () => void
canExecute: boolean
canExecuteCancel: boolean
}
const OwnersColumn = ({
tx,
cancelTx = makeTransaction({ isCancellationTx: true, status: TransactionStatus.AWAITING_YOUR_CONFIRMATION }),
classes,
thresholdReached,
cancelThresholdReached,
onTxConfirm,
@ -85,7 +109,8 @@ const OwnersColumn = ({
onTxReject,
canExecute,
canExecuteCancel,
}) => {
}: ownersColumnProps): React.ReactElement => {
const classes = useStyles()
let showOlderTxAnnotation
if (tx.isExecuted || cancelTx.isExecuted) {
@ -234,4 +259,4 @@ const OwnersColumn = ({
)
}
export default withStyles(styles as any)(OwnersColumn)
export default OwnersColumn

View File

@ -1,6 +1,7 @@
import { boldFont, border, error, primary, secondary, secondaryText, sm, warning } from 'src/theme/variables'
import { createStyles } from '@material-ui/core/styles'
export const styles = () => ({
export const styles = createStyles({
ownersList: {
height: '192px',
overflowY: 'scroll',
@ -18,7 +19,7 @@ export const styles = () => ({
position: 'absolute',
top: '-27px',
width: '2px',
zIndex: '12',
zIndex: 12,
},
verticalLinePending: {
backgroundColor: secondaryText,
@ -80,7 +81,7 @@ export const styles = () => ({
justifyContent: 'center',
marginRight: '18px',
width: '20px',
zIndex: '13',
zIndex: 13,
'& > img': {
display: 'block',
@ -88,7 +89,7 @@ export const styles = () => ({
},
button: {
alignSelf: 'center',
flexGrow: '0',
flexGrow: 0,
fontSize: '16px',
justifyContent: 'center',
paddingLeft: '14px',

View File

@ -22,9 +22,12 @@ import Paragraph from 'src/components/layout/Paragraph'
import LinkWithRef from 'src/components/layout/Link'
import { shortVersionOf } from 'src/logic/wallets/ethAddresses'
import { Transaction } from 'src/routes/safe/store/models/types/transaction'
import { DataDecoded } from 'src/routes/safe/store/models/types/transactions.d'
import DividerLine from 'src/components/DividerLine'
export const TRANSACTIONS_DESC_CUSTOM_VALUE_TEST_ID = 'tx-description-custom-value'
export const TRANSACTIONS_DESC_CUSTOM_DATA_TEST_ID = 'tx-description-custom-data'
export const TRANSACTION_DESC_ACTION_TEST_ID = 'tx-description-action-data'
const useStyles = makeStyles(styles)
@ -45,42 +48,55 @@ const TxInfo = styled.div`
padding: 8px 8px 8px 16px;
`
const MultiSendCustomData = ({ tx, order }: { tx: MultiSendDetails; order: number }): React.ReactElement => {
const TxInfoDetails = ({ data }: { data: DataDecoded }): React.ReactElement => (
<TxInfo>
<TxDetailsMethodName size="lg" strong>
{data.method}
</TxDetailsMethodName>
{data.parameters.map((param, index) => (
<TxDetailsMethodParam key={`${data.method}_param-${index}`}>
<InlineText size="lg" strong>
{param.name}({param.type}):
</InlineText>
<Value method={data.method} type={param.type} value={param.value} />
</TxDetailsMethodParam>
))}
</TxInfo>
)
const MultiSendCustomDataAction = ({ tx, order }: { tx: MultiSendDetails; order: number }): React.ReactElement => {
const classes = useStyles()
const methodName = tx.data?.method ? ` (${tx.data.method})` : ''
return (
<>
<Collapse
collapseClassName={classes.collapse}
headerWrapperClassName={classes.collapseHeaderWrapper}
title={<IconText iconSize="sm" iconType="code" text={`Action ${order + 1}${methodName}`} textSize="lg" />}
>
<TxDetailsContent>
<TxInfo>
<Bold>Send {humanReadableValue(tx.value)} ETH to:</Bold>
<OwnerAddressTableCell address={tx.to} showLinks />
</TxInfo>
{tx.data && (
<TxInfo>
<TxDetailsMethodName size="lg">
<strong>{tx.data.method}</strong>
</TxDetailsMethodName>
{tx.data?.parameters.map((param, index) => (
<TxDetailsMethodParam key={`${tx.operation}_${tx.to}_${tx.data.method}_param-${index}`}>
<InlineText size="lg">
<strong>
{param.name}({param.type}):
</strong>
</InlineText>
<Value method={methodName} type={param.type} value={param.value} />
</TxDetailsMethodParam>
))}
</TxInfo>
)}
</TxDetailsContent>
</Collapse>
</>
<Collapse
collapseClassName={classes.collapse}
headerWrapperClassName={classes.collapseHeaderWrapper}
title={<IconText iconSize="sm" iconType="code" text={`Action ${order + 1}${methodName}`} textSize="lg" />}
>
<TxDetailsContent>
<TxInfo>
<Bold>Send {humanReadableValue(tx.value)} ETH to:</Bold>
<OwnerAddressTableCell address={tx.to} showLinks />
</TxInfo>
{!!tx.data && <TxInfoDetails data={tx.data} />}
</TxDetailsContent>
</Collapse>
)
}
const MultiSendCustomData = ({ txDetails }: { txDetails: MultiSendDetails[] }): React.ReactElement => {
const classes = useStyles()
return (
<Block className={classes.multiSendTxData} data-testid={TRANSACTIONS_DESC_CUSTOM_DATA_TEST_ID}>
{txDetails.map((tx, index) => (
<MultiSendCustomDataAction key={`${tx.to}-row-${index}`} tx={tx} order={index} />
))}
</Block>
)
}
@ -128,13 +144,31 @@ const TxData = ({ data }: { data: string }): React.ReactElement => {
)
}
const TxActionData = ({ dataDecoded }: { dataDecoded: DataDecoded }): React.ReactElement => {
const classes = useStyles()
return (
<>
<DividerLine withArrow={false} />
<Block className={classes.txData} data-testid={TRANSACTION_DESC_ACTION_TEST_ID}>
<Bold>Action</Bold>
<TxInfoDetails data={dataDecoded} />
</Block>
<DividerLine withArrow={false} />
</>
)
}
interface GenericCustomDataProps {
amount?: string
data: string
recipient: string
storedTx: Transaction
}
const GenericCustomData = ({ amount = '0', data, recipient }: GenericCustomDataProps): React.ReactElement => {
const GenericCustomData = ({ amount = '0', data, recipient, storedTx }: GenericCustomDataProps): React.ReactElement => {
const classes = useStyles()
const recipientName = useSelector((state) => getNameFromAddressBook(state, recipient))
@ -148,6 +182,9 @@ const GenericCustomData = ({ amount = '0', data, recipient }: GenericCustomDataP
<EtherscanLink knownAddress={false} type="address" value={recipient} />
)}
</Block>
{!!storedTx?.dataDecoded && <TxActionData dataDecoded={storedTx.dataDecoded} />}
<Block className={classes.txData} data-testid={TRANSACTIONS_DESC_CUSTOM_DATA_TEST_ID}>
<Bold>Data (hex encoded):</Bold>
<TxData data={data} />
@ -164,16 +201,12 @@ interface CustomDescriptionProps {
}
const CustomDescription = ({ amount, data, recipient, storedTx }: CustomDescriptionProps): React.ReactElement => {
const classes = useStyles()
const txDetails = (storedTx.multiSendTx && extractMultiSendDecodedData(storedTx).txDetails) ?? undefined
return storedTx.multiSendTx ? (
<Block className={classes.multiSendTxData} data-testid={TRANSACTIONS_DESC_CUSTOM_DATA_TEST_ID}>
{extractMultiSendDecodedData(storedTx).txDetails?.map((tx, index) => (
<MultiSendCustomData key={`${tx.to}-row-${index}`} tx={tx} order={index} />
))}
</Block>
return txDetails ? (
<MultiSendCustomData txDetails={txDetails} />
) : (
<GenericCustomData amount={amount} data={data} recipient={recipient} />
<GenericCustomData amount={amount} data={data} recipient={recipient} storedTx={storedTx} />
)
}

View File

@ -8,7 +8,7 @@ import SettingsTxIcon from './assets/settings.svg'
import CustomIconText from 'src/components/CustomIconText'
import { getAppInfoFromOrigin, getAppInfoFromUrl } from 'src/routes/safe/components/Apps/utils'
import { SafeApp } from 'src/routes/safe/components/Apps/types'
import { SafeApp } from 'src/routes/safe/components/Apps/types.d'
const typeToIcon = {
outgoing: OutgoingTxIcon,

View File

@ -8,7 +8,7 @@ import React from 'react'
import TxType from './TxType'
import { buildOrderFieldFrom } from 'src/components/Table/sorting'
import { TableColumn } from 'src/components/Table/types'
import { TableColumn } from 'src/components/Table/types.d'
import { formatAmount } from 'src/logic/tokens/utils/formatAmount'
import { INCOMING_TX_TYPES } from 'src/routes/safe/store/models/incomingTransaction'
import { Transaction } from 'src/routes/safe/store/models/types/transaction'

View File

@ -13,8 +13,7 @@ describe('TxsTable Columns > getTxTableData', () => {
const txRow = txTableData.first()
// Then
// expect(txRow[TX_TABLE_RAW_CANCEL_TX_ID]).toEqual(mockedCancelTransaction)
expect(txRow[TX_TABLE_RAW_CANCEL_TX_ID]).toBeUndefined()
expect(txRow[TX_TABLE_RAW_CANCEL_TX_ID]).toEqual(mockedCancelTransaction)
})
it('should not include CancelTx object inside TxTableData', () => {
// Given

View File

@ -8,7 +8,7 @@ import activateAssetsByBalance from 'src/logic/tokens/store/actions/activateAsse
import fetchSafeTokens from 'src/logic/tokens/store/actions/fetchSafeTokens'
import { fetchTokens } from 'src/logic/tokens/store/actions/fetchTokens'
import { COINS_LOCATION_REGEX, COLLECTIBLES_LOCATION_REGEX } from 'src/routes/safe/components/Balances'
import { Dispatch } from 'src/routes/safe/store/actions/types'
import { Dispatch } from 'src/routes/safe/store/actions/types.d'
export const useFetchTokens = (safeAddress: string): void => {
const dispatch = useDispatch<Dispatch>()

View File

@ -8,7 +8,7 @@ import fetchLatestMasterContractVersion from 'src/routes/safe/store/actions/fetc
import fetchSafe from 'src/routes/safe/store/actions/fetchSafe'
import fetchTransactions from 'src/routes/safe/store/actions/transactions/fetchTransactions'
import fetchSafeCreationTx from 'src/routes/safe/store/actions/fetchSafeCreationTx'
import { Dispatch } from 'src/routes/safe/store/actions/types'
import { Dispatch } from 'src/routes/safe/store/actions/types.d'
export const useLoadSafe = (safeAddress: string): void => {
const dispatch = useDispatch<Dispatch>()

View File

@ -1,56 +1,54 @@
import { getNewTxNonce, shouldExecuteTransaction } from 'src/routes/safe/store/actions/utils'
import { GnosisSafe } from 'src/types/contracts/GnosisSafe.d'
import { TxServiceModel } from 'src/routes/safe/store/actions/transactions/fetchTransactions/loadOutgoingTransactions'
describe('Store actions utils > getNewTxNonce', () => {
it(`should return txNonce if it's a valid value`, async () => {
it(`Should return passed predicted transaction nonce if it's a valid value`, async () => {
// Given
const txNonce = '45'
const lastTx = {
nonce: 44
}
const safeInstance = {
nonce: () => Promise.resolve({
toString: () => Promise.resolve('45')
})
}
const lastTx = { nonce: 44 } as TxServiceModel
const safeInstance = {}
// When
const nonce = await getNewTxNonce(txNonce, lastTx, safeInstance)
const nonce = await getNewTxNonce(txNonce, lastTx, safeInstance as GnosisSafe)
// Then
expect(nonce).toBe('45')
})
it(`should return lastTx.nonce + 1 if txNonce is not valid`, async () => {
it(`Should return nonce of a last transaction + 1 if passed nonce is less than last transaction or invalid`, async () => {
// Given
const txNonce = ''
const lastTx = {
nonce: 44
}
const lastTx = { nonce: 44 } as TxServiceModel
const safeInstance = {
nonce: () => Promise.resolve({
toString: () => Promise.resolve('45')
})
methods: {
nonce: () => ({
call: () => Promise.resolve('45'),
}),
},
}
// When
const nonce = await getNewTxNonce(txNonce, lastTx, safeInstance)
const nonce = await getNewTxNonce(txNonce, lastTx, safeInstance as GnosisSafe)
// Then
expect(nonce).toBe('45')
})
it(`should retrieve contract's instance nonce value, if txNonce and lastTx are not valid`, async () => {
it(`Should retrieve contract's instance nonce value as a fallback, if txNonce and lastTx are not valid`, async () => {
// Given
const txNonce = ''
const lastTx = null
const safeInstance = {
nonce: () => Promise.resolve({
toString: () => Promise.resolve('45')
})
methods: {
nonce: () => ({
call: () => Promise.resolve('45'),
}),
},
}
// When
const nonce = await getNewTxNonce(txNonce, lastTx, safeInstance)
const nonce = await getNewTxNonce(txNonce, lastTx, safeInstance as GnosisSafe)
// Then
expect(nonce).toBe('45')
@ -61,17 +59,17 @@ describe('Store actions utils > shouldExecuteTransaction', () => {
it(`should return false if there's a previous tx pending to be executed`, async () => {
// Given
const safeInstance = {
getThreshold: () => Promise.resolve({
toNumber: () => 1
})
methods: {
getThreshold: () => ({
call: () => Promise.resolve('1'),
}),
},
}
const nonce = '1'
const lastTx = {
isExecuted: false
}
const lastTx = { isExecuted: false } as TxServiceModel
// When
const isExecution = await shouldExecuteTransaction(safeInstance, nonce, lastTx)
const isExecution = await shouldExecuteTransaction(safeInstance as GnosisSafe, nonce, lastTx)
// Then
expect(isExecution).toBeFalsy()
@ -80,17 +78,17 @@ describe('Store actions utils > shouldExecuteTransaction', () => {
it(`should return false if threshold is greater than 1`, async () => {
// Given
const safeInstance = {
getThreshold: () => Promise.resolve({
toNumber: () => 2
})
methods: {
getThreshold: () => ({
call: () => Promise.resolve('2'),
}),
},
}
const nonce = '1'
const lastTx = {
isExecuted: true
}
const lastTx = { isExecuted: true } as TxServiceModel
// When
const isExecution = await shouldExecuteTransaction(safeInstance, nonce, lastTx)
const isExecution = await shouldExecuteTransaction(safeInstance as GnosisSafe, nonce, lastTx)
// Then
expect(isExecution).toBeFalsy()
@ -99,17 +97,17 @@ describe('Store actions utils > shouldExecuteTransaction', () => {
it(`should return true is threshold is 1 and previous tx is executed`, async () => {
// Given
const safeInstance = {
getThreshold: () => Promise.resolve({
toNumber: () => 1
})
methods: {
getThreshold: () => ({
call: () => Promise.resolve('1'),
}),
},
}
const nonce = '1'
const lastTx = {
isExecuted: true
}
const lastTx = { isExecuted: true } as TxServiceModel
// When
const isExecution = await shouldExecuteTransaction(safeInstance, nonce, lastTx)
const isExecution = await shouldExecuteTransaction(safeInstance as GnosisSafe, nonce, lastTx)
// Then
expect(isExecution).toBeTruthy()

View File

@ -1,7 +0,0 @@
import { createAction } from 'redux-actions'
export const ADD_SAFE_MODULES = 'ADD_SAFE_MODULES'
const addSafeModules = createAction(ADD_SAFE_MODULES)
export default addSafeModules

View File

@ -15,7 +15,6 @@ import { makeOwner } from 'src/routes/safe/store/models/owner'
import { checksumAddress } from 'src/utils/checksumAddress'
import { ModulePair, SafeOwner } from 'src/routes/safe/store/models/safe'
import { Dispatch } from 'redux'
import addSafeModules from './addSafeModules'
import { SENTINEL_ADDRESS } from 'src/logic/contracts/safeContracts'
const buildOwnersFrom = (
@ -49,7 +48,7 @@ const buildModulesLinkedList = (modules: string[] | undefined, nextModule: strin
return null
}
export const buildSafe = async (safeAdd, safeName, latestMasterContractVersion?: any) => {
export const buildSafe = async (safeAdd: string, safeName: string, latestMasterContractVersion?: any) => {
const safeAddress = checksumAddress(safeAdd)
const safeParams = ['getThreshold', 'nonce', 'VERSION', 'getOwners']
@ -105,24 +104,16 @@ export const checkAndUpdateSafe = (safeAdd: string) => async (dispatch: Dispatch
// Converts from [ { address, ownerName} ] to address array
const localOwners = localSafe ? localSafe.owners.map((localOwner) => localOwner.address) : undefined
const localThreshold = localSafe ? localSafe.threshold : undefined
const localNonce = localSafe ? localSafe.nonce : undefined
dispatch(
addSafeModules({
safeAddress,
modulesAddresses: buildModulesLinkedList(modules?.array, modules?.next),
updateSafe({
address: safeAddress,
modules: buildModulesLinkedList(modules?.array, modules?.next),
nonce: Number(remoteNonce),
threshold: Number(remoteThreshold),
}),
)
if (localNonce !== Number(remoteNonce)) {
dispatch(updateSafe({ address: safeAddress, nonce: Number(remoteNonce) }))
}
if (localThreshold !== Number(remoteThreshold)) {
dispatch(updateSafe({ address: safeAddress, threshold: Number(remoteThreshold) }))
}
// If the remote owners does not contain a local address, we remove that local owner
if (localOwners) {
localOwners.forEach((localAddress) => {
@ -149,7 +140,7 @@ export const checkAndUpdateSafe = (safeAdd: string) => async (dispatch: Dispatch
}
// eslint-disable-next-line consistent-return
export default (safeAdd) => async (dispatch, getState) => {
export default (safeAdd: string) => async (dispatch, getState) => {
try {
const safeAddress = checksumAddress(safeAdd)
const safeName = (await getSafeName(safeAddress)) || 'LOADED SAFE'

View File

@ -1,6 +1,5 @@
import { List, Map } from 'immutable'
import { decodeMethods } from 'src/logic/contracts/methodIds'
import { TOKEN_REDUCER_ID } from 'src/logic/tokens/store/reducer/tokens'
import {
getERC20DecimalsAndSymbol,
@ -318,30 +317,6 @@ export type TxToMock = TxArgs & {
}
export const mockTransaction = (tx: TxToMock, safeAddress: string, state: AppReduxState): Promise<Transaction> => {
const submissionDate = new Date().toISOString()
const transactionStructure: TxServiceModel = {
blockNumber: null,
confirmationsRequired: null,
dataDecoded: decodeMethods(tx.data),
ethGasPrice: null,
executionDate: null,
executor: null,
fee: null,
gasUsed: null,
isExecuted: false,
isSuccessful: null,
modified: submissionDate,
origin: null,
safe: safeAddress,
safeTxHash: null,
signatures: null,
submissionDate,
transactionHash: null,
confirmations: [],
...tx,
}
const knownTokens: Map<string, Token> = state[TOKEN_REDUCER_ID]
const safe: SafeRecord = state[SAFE_REDUCER_ID].getIn(['safes', safeAddress])
const cancellationTxs = state[CANCELLATION_TRANSACTIONS_REDUCER_ID].get(safeAddress) || Map()
@ -353,7 +328,7 @@ export const mockTransaction = (tx: TxToMock, safeAddress: string, state: AppRed
knownTokens,
outgoingTxs,
safe,
tx: transactionStructure,
tx: (tx as unknown) as TxServiceModel,
txCode: EMPTY_DATA,
})
}

View File

@ -15,7 +15,6 @@ import { makeOwner } from 'src/routes/safe/store/models/owner'
import makeSafe from 'src/routes/safe/store/models/safe'
import { checksumAddress } from 'src/utils/checksumAddress'
import { SafeReducerMap } from './types/safe'
import { ADD_SAFE_MODULES } from 'src/routes/safe/store/actions/addSafeModules'
export const SAFE_REDUCER_ID = 'safes'
export const DEFAULT_SAFE_INITIAL_STATE = 'NOT_ASKED'
@ -128,10 +127,6 @@ export default handleActions(
return prevSafe.merge({ owners: updatedOwners })
})
},
[ADD_SAFE_MODULES]: (state: SafeReducerMap, action) => {
const { modulesAddresses, safeAddress } = action.payload
return state.setIn(['safes', safeAddress, 'modules'], modulesAddresses)
},
[SET_DEFAULT_SAFE]: (state: SafeReducerMap, action) => state.set('defaultSafe', action.payload),
[SET_LATEST_MASTER_CONTRACT_VERSION]: (state: SafeReducerMap, action) =>
state.set('latestMasterContractVersion', action.payload),
@ -143,4 +138,4 @@ export default handleActions(
}),
)
export * from './types/safe.d'
export * from './types/safe'

View File

@ -9,11 +9,13 @@ export interface SafeReducerState {
latestMasterContractVersion: string
}
interface SafeReducerStateSerialized extends SafeReducerState {
interface SafeReducerStateJSON {
defaultSafe: 'NOT_ASKED' | string | undefined
safes: Record<string, SafeRecordProps>
latestMasterContractVersion: string
}
export interface SafeReducerMap extends Map<string, any> {
toJS(): SafeReducerStateSerialized
toJS(): SafeReducerStateJSON
get<K extends keyof SafeReducerState>(key: K): SafeReducerState[K]
}

View File

@ -17,7 +17,7 @@ import cookies, { COOKIES_REDUCER_ID } from 'src/logic/cookies/store/reducer/coo
import currencyValuesStorageMiddleware from 'src/logic/currencyValues/store/middleware'
import currencyValues, {
CURRENCY_VALUES_KEY,
CurrencyReducerMap,
CurrencyValuesState,
} from 'src/logic/currencyValues/store/reducer/currencyValues'
import currentSession, { CURRENT_SESSION_REDUCER_ID } from 'src/logic/currentSession/store/reducer/currentSession'
import notifications, { NOTIFICATIONS_REDUCER_ID } from 'src/logic/notifications/store/reducer/notifications'
@ -80,7 +80,7 @@ export type AppReduxState = CombinedState<{
[CANCELLATION_TRANSACTIONS_REDUCER_ID]: CancellationTxState
[INCOMING_TRANSACTIONS_REDUCER_ID]: Map<string, any>
[NOTIFICATIONS_REDUCER_ID]: Map<string, any>
[CURRENCY_VALUES_KEY]: CurrencyReducerMap
[CURRENCY_VALUES_KEY]: CurrencyValuesState
[COOKIES_REDUCER_ID]: Map<string, any>
[ADDRESS_BOOK_REDUCER_ID]: AddressBookReducerMap
[CURRENT_SESSION_REDUCER_ID]: Map<string, any>

View File

@ -24,10 +24,7 @@ export const loadFromStorage = async <T = unknown>(key: string): Promise<T | und
}
}
export const saveToStorage = async (
key: string,
value: Record<string, unknown> | boolean | string | number | Array<unknown>,
): Promise<void> => {
export const saveToStorage = async <T = unknown>(key: string, value: T): Promise<void> => {
try {
const stringifiedValue = JSON.stringify(value)
await storage.set(`${PREFIX}__${key}`, stringifiedValue)

867
yarn.lock

File diff suppressed because it is too large Load Diff