Merge branch 'development' of github.com:gnosis/safe-react into fix/desktop-CI
This commit is contained in:
commit
44782ee7e2
|
@ -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)) {
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
|
@ -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'
|
||||
|
|
@ -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,
|
||||
|
|
|
@ -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 }
|
|
@ -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 }
|
|
@ -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 }
|
|
@ -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' }}>
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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} />
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>()
|
||||
|
|
|
@ -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>()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -143,4 +143,4 @@ export default handleActions(
|
|||
}),
|
||||
)
|
||||
|
||||
export * from './types/safe.d'
|
||||
export * from './types/safe'
|
||||
|
|
|
@ -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]
|
||||
}
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue