Safe apps: return safe tx hash (#1245)

* apps refactoring wip

* apps refactoring wip

* type fixes

* add useLegalConsent hook in apps

* useAppList hook wip

* dep nump

* useAppList hook wip

* fix selecting first app

* Remove console.log

* dep bump

* update persisting app logic

* update saveToStorage type

* fix crash on apps tab

* add appframe comp

* add handleIframeLoad func

* reuse selectedApp variable in hook

* remove initialAppSelected

* yarn regenration

* useIframeCommunicator wip

* add types for apps component

* dep bump

* fix history types

* yarn regenration

* extract useIframeMessenger hook

* fix safe-react-components version

* useIframeMessageHandler wip

* fix types

* send safe info on handshake

* fix naming/types for url utils

* remove operations

* update safe-apps-sdk

* wip

* update safe-apps-sdk

* requestId wip

* cta snackbar usage fixes

* notifications refactor wip

* notifications refactor: use dispatch

* tsc fixes

* extract confirm transaction modal

* Extract confirmation modal to a separate component

* dep bump

* ConfirmTransactionModal component

* Return safeTxHash after user confirmed transaction

* fix address validator, close modal when user confirms the tx

* close modal after confirmation

* update imports

* update imports [2]

* update imports [3]

* update imports [4]

* remove console.log in createTransaction

* update safe-apps-sdk

* yarn.lock

* EditOwnerModal types
This commit is contained in:
Mikhail Mikheev 2020-08-26 13:05:34 +04:00 committed by GitHub
parent 4dc28942c0
commit 66c5ae7f8e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 1298 additions and 1215 deletions

View File

@ -162,7 +162,7 @@
]
},
"dependencies": {
"@gnosis.pm/safe-apps-sdk": "0.3.1",
"@gnosis.pm/safe-apps-sdk": "https://github.com/gnosis/safe-apps-sdk.git#development",
"@gnosis.pm/safe-contracts": "1.1.1-dev.2",
"@gnosis.pm/safe-react-components": "https://github.com/gnosis/safe-react-components.git#fd4498f",
"@gnosis.pm/util-contracts": "2.0.6",
@ -174,7 +174,7 @@
"async-sema": "^3.1.0",
"axios": "0.19.2",
"bignumber.js": "9.0.0",
"bnc-onboard": "1.11.0",
"bnc-onboard": "1.11.1",
"classnames": "^2.2.6",
"concurrently": "^5.2.0",
"connected-react-router": "6.8.0",
@ -236,14 +236,14 @@
"@types/history": "4.6.2",
"@types/jest": "^26.0.9",
"@types/lodash.memoize": "^4.1.6",
"@types/node": "14.0.27",
"@types/node": "14.6.0",
"@types/react": "^16.9.44",
"@types/react-dom": "^16.9.6",
"@types/react-redux": "^7.1.9",
"@types/react-router-dom": "^5.1.5",
"@types/styled-components": "^5.1.2",
"@typescript-eslint/eslint-plugin": "3.8.0",
"@typescript-eslint/parser": "3.8.0",
"@typescript-eslint/eslint-plugin": "3.9.1",
"@typescript-eslint/parser": "3.9.1",
"autoprefixer": "9.8.6",
"cross-env": "^7.0.2",
"dotenv": "^8.2.0",

View File

@ -1,6 +0,0 @@
import { fetchProvider, removeProvider } from 'src/logic/wallets/store/actions'
export default {
fetchProvider,
removeProvider,
}

View File

@ -1,74 +1,64 @@
import { withSnackbar } from 'notistack'
import * as React from 'react'
import { connect } from 'react-redux'
import React, { useEffect } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import actions from './actions'
import Layout from './components/Layout'
import ConnectDetails from './components/ProviderDetails/ConnectDetails'
import UserDetails from './components/ProviderDetails/UserDetails'
import ProviderAccessible from './components/ProviderInfo/ProviderAccessible'
import ProviderDisconnected from './components/ProviderInfo/ProviderDisconnected'
import selector from './selector'
import {
availableSelector,
loadedSelector,
networkSelector,
providerNameSelector,
userAccountSelector,
} from 'src/logic/wallets/store/selectors'
import { removeProvider } from 'src/logic/wallets/store/actions'
import { onboard } from 'src/components/ConnectButton'
import { NOTIFICATIONS, showSnackbar } from 'src/logic/notifications'
import { loadLastUsedProvider } from 'src/logic/wallets/store/middlewares/providerWatcher'
import { logComponentStack } from 'src/utils/logBoundaries'
class HeaderComponent extends React.PureComponent<any, any> {
constructor(props) {
super(props)
const HeaderComponent = (): React.ReactElement => {
const provider = useSelector(providerNameSelector)
const userAddress = useSelector(userAccountSelector)
const network = useSelector(networkSelector)
const loaded = useSelector(loadedSelector)
const available = useSelector(availableSelector)
const dispatch = useDispatch()
this.state = {
hasError: false,
}
}
async componentDidMount() {
const lastUsedProvider = await loadLastUsedProvider()
if (lastUsedProvider) {
const hasSelectedWallet = await onboard.walletSelect(lastUsedProvider)
if (hasSelectedWallet) {
await onboard.walletCheck()
useEffect(() => {
const tryToConnectToLastUsedProvider = async () => {
const lastUsedProvider = await loadLastUsedProvider()
if (lastUsedProvider) {
const hasSelectedWallet = await onboard.walletSelect(lastUsedProvider)
if (hasSelectedWallet) {
await onboard.walletCheck()
}
}
}
}
componentDidCatch(error, info) {
const { closeSnackbar, enqueueSnackbar } = this.props
tryToConnectToLastUsedProvider()
}, [])
this.setState({ hasError: true })
showSnackbar(NOTIFICATIONS.CONNECT_WALLET_ERROR_MSG, enqueueSnackbar, closeSnackbar)
logComponentStack(error, info)
}
getOpenDashboard = () => {
const openDashboard = () => {
const { wallet } = onboard.getState()
return wallet.type === 'sdk' && wallet.dashboard
}
onDisconnect = () => {
const { closeSnackbar, enqueueSnackbar, removeProvider } = this.props
removeProvider(enqueueSnackbar, closeSnackbar)
const onDisconnect = () => {
dispatch(removeProvider())
}
getProviderInfoBased = () => {
const { hasError } = this.state
const { available, loaded, provider, userAddress, network } = this.props
if (hasError || !loaded) {
const getProviderInfoBased = () => {
if (!loaded) {
return <ProviderDisconnected />
}
return <ProviderAccessible connected={available} provider={provider} network={network} userAddress={userAddress} />
}
getProviderDetailsBased = () => {
const { hasError } = this.state
const { available, loaded, network, provider, userAddress } = this.props
if (hasError || !loaded) {
const getProviderDetailsBased = () => {
if (!loaded) {
return <ConnectDetails />
}
@ -76,20 +66,18 @@ class HeaderComponent extends React.PureComponent<any, any> {
<UserDetails
connected={available}
network={network}
onDisconnect={this.onDisconnect}
openDashboard={this.getOpenDashboard()}
onDisconnect={onDisconnect}
openDashboard={openDashboard()}
provider={provider}
userAddress={userAddress}
/>
)
}
render() {
const info = this.getProviderInfoBased()
const details = this.getProviderDetailsBased()
const info = getProviderInfoBased()
const details = getProviderDetailsBased()
return <Layout providerDetails={details} providerInfo={info} />
}
return <Layout providerDetails={details} providerInfo={info} />
}
export default connect(selector, actions)(withSnackbar(HeaderComponent))
export default HeaderComponent

View File

@ -1,17 +0,0 @@
import { createStructuredSelector } from 'reselect'
import {
availableSelector,
loadedSelector,
networkSelector,
providerNameSelector,
userAccountSelector,
} from 'src/logic/wallets/store/selectors'
export default createStructuredSelector({
provider: providerNameSelector,
userAddress: userAccountSelector,
network: networkSelector,
loaded: loadedSelector,
available: availableSelector,
})

View File

@ -59,7 +59,7 @@ export const ok = (): undefined => undefined
export const mustBeEthereumAddress = memoize(
(address: string): ValidatorReturnType => {
const startsWith0x = address.startsWith('0x')
const startsWith0x = address?.startsWith('0x')
const isAddress = getWeb3().utils.isAddress(address)
return startsWith0x && isAddress ? undefined : 'Address should be a valid Ethereum address or ENS name'

View File

@ -2,14 +2,14 @@ import { IconButton } from '@material-ui/core'
import { Close as IconClose } from '@material-ui/icons'
import * as React from 'react'
import { NOTIFICATIONS } from './notificationTypes'
import { Notification, NOTIFICATIONS } from './notificationTypes'
import closeSnackbarAction from 'src/logic/notifications/store/actions/closeSnackbar'
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
import { getAppInfoFromOrigin } from 'src/routes/safe/components/Apps/utils'
import { store } from 'src/store'
const setNotificationOrigin = (notification, origin) => {
const setNotificationOrigin = (notification: Notification, origin: string): Notification => {
if (!origin) {
return notification
}
@ -18,18 +18,18 @@ const setNotificationOrigin = (notification, origin) => {
return { ...notification, message: `${appInfo.name}: ${notification.message}` }
}
const getStandardTxNotificationsQueue = (origin) => {
return {
beforeExecution: setNotificationOrigin(NOTIFICATIONS.SIGN_TX_MSG, origin),
pendingExecution: setNotificationOrigin(NOTIFICATIONS.TX_PENDING_MSG, origin),
afterRejection: setNotificationOrigin(NOTIFICATIONS.TX_REJECTED_MSG, origin),
afterExecution: {
noMoreConfirmationsNeeded: setNotificationOrigin(NOTIFICATIONS.TX_EXECUTED_MSG, origin),
moreConfirmationsNeeded: setNotificationOrigin(NOTIFICATIONS.TX_EXECUTED_MORE_CONFIRMATIONS_MSG, origin),
},
afterExecutionError: setNotificationOrigin(NOTIFICATIONS.TX_FAILED_MSG, origin),
}
}
const getStandardTxNotificationsQueue = (
origin: string,
): Record<string, Record<string, Notification> | Notification> => ({
beforeExecution: setNotificationOrigin(NOTIFICATIONS.SIGN_TX_MSG, origin),
pendingExecution: setNotificationOrigin(NOTIFICATIONS.TX_PENDING_MSG, origin),
afterRejection: setNotificationOrigin(NOTIFICATIONS.TX_REJECTED_MSG, origin),
afterExecution: {
noMoreConfirmationsNeeded: setNotificationOrigin(NOTIFICATIONS.TX_EXECUTED_MSG, origin),
moreConfirmationsNeeded: setNotificationOrigin(NOTIFICATIONS.TX_EXECUTED_MORE_CONFIRMATIONS_MSG, origin),
},
afterExecutionError: setNotificationOrigin(NOTIFICATIONS.TX_FAILED_MSG, origin),
})
const waitingTransactionNotificationsQueue = {
beforeExecution: null,
@ -40,7 +40,7 @@ const waitingTransactionNotificationsQueue = {
afterExecutionError: null,
}
const getConfirmationTxNotificationsQueue = (origin) => {
const getConfirmationTxNotificationsQueue = (origin: string) => {
return {
beforeExecution: setNotificationOrigin(NOTIFICATIONS.SIGN_TX_MSG, origin),
pendingExecution: setNotificationOrigin(NOTIFICATIONS.TX_CONFIRMATION_PENDING_MSG, origin),
@ -53,7 +53,7 @@ const getConfirmationTxNotificationsQueue = (origin) => {
}
}
const getCancellationTxNotificationsQueue = (origin) => {
const getCancellationTxNotificationsQueue = (origin: string) => {
return {
beforeExecution: setNotificationOrigin(NOTIFICATIONS.SIGN_TX_MSG, origin),
pendingExecution: setNotificationOrigin(NOTIFICATIONS.TX_PENDING_MSG, origin),
@ -199,9 +199,13 @@ export const getNotificationsFromTxType: any = (txType, origin) => {
return notificationsQueue
}
export const enhanceSnackbarForAction: any = (notification, key, onClick) => ({
export const enhanceSnackbarForAction = (
notification: Notification,
key?: number | string,
onClick?: () => void,
): Notification => ({
...notification,
key,
key: key || notification.key,
options: {
...notification.options,
onClick,
@ -213,14 +217,3 @@ export const enhanceSnackbarForAction: any = (notification, key, onClick) => ({
),
},
})
export const showSnackbar: any = (notification, enqueueSnackbar, closeSnackbar) =>
enqueueSnackbar(notification.message, {
...notification.options,
// eslint-disable-next-line react/display-name
action: (key) => (
<IconButton onClick={() => closeSnackbar(key)}>
<IconClose />
</IconButton>
),
})

View File

@ -1,3 +1,5 @@
import { OptionsObject } from 'notistack'
import { getNetwork } from 'src/config'
import { capitalize } from 'src/utils/css'
@ -9,7 +11,50 @@ export const INFO = 'info'
const shortDuration = 5000
const longDuration = 10000
export const NOTIFICATIONS = {
export type NotificationId = keyof typeof NOTIFICATION_IDS
export type Notification = {
message: string
options: OptionsObject
key?: number | string
}
const NOTIFICATION_IDS = {
CONNECT_WALLET_MSG: 'CONNECT_WALLET_MSG',
CONNECT_WALLET_READ_MODE_MSG: 'CONNECT_WALLET_READ_MODE_MSG',
WALLET_CONNECTED_MSG: 'WALLET_CONNECTED_MSG',
WALLET_DISCONNECTED_MSG: 'WALLET_DISCONNECTED_MSG',
UNLOCK_WALLET_MSG: 'UNLOCK_WALLET_MSG',
CONNECT_WALLET_ERROR_MSG: 'CONNECT_WALLET_ERROR_MSG',
SIGN_TX_MSG: 'SIGN_TX_MSG',
TX_PENDING_MSG: 'TX_PENDING_MSG',
TX_REJECTED_MSG: 'TX_REJECTED_MSG',
TX_EXECUTED_MSG: 'TX_EXECUTED_MSG',
TX_CANCELLATION_EXECUTED_MSG: 'TX_CANCELLATION_EXECUTED_MSG',
TX_FAILED_MSG: 'TX_FAILED_MSG',
TX_EXECUTED_MORE_CONFIRMATIONS_MSG: 'TX_EXECUTED_MORE_CONFIRMATIONS_MSG',
TX_WAITING_MSG: 'TX_WAITING_MSG',
TX_INCOMING_MSG: 'TX_INCOMING_MSG',
TX_CONFIRMATION_PENDING_MSG: 'TX_CONFIRMATION_PENDING_MSG',
TX_CONFIRMATION_EXECUTED_MSG: 'TX_CONFIRMATION_EXECUTED_MSG',
TX_CONFIRMATION_FAILED_MSG: 'TX_CONFIRMATION_FAILED_MSG',
SAFE_NAME_CHANGED_MSG: 'SAFE_NAME_CHANGED_MSG',
OWNER_NAME_CHANGE_EXECUTED_MSG: 'OWNER_NAME_CHANGE_EXECUTED_MSG',
SIGN_SETTINGS_CHANGE_MSG: 'SIGN_SETTINGS_CHANGE_MSG',
SETTINGS_CHANGE_PENDING_MSG: 'SETTINGS_CHANGE_PENDING_MSG',
SETTINGS_CHANGE_REJECTED_MSG: 'SETTINGS_CHANGE_REJECTED_MSG',
SETTINGS_CHANGE_EXECUTED_MSG: 'SETTINGS_CHANGE_EXECUTED_MSG',
SETTINGS_CHANGE_EXECUTED_MORE_CONFIRMATIONS_MSG: 'SETTINGS_CHANGE_EXECUTED_MORE_CONFIRMATIONS_MSG',
SETTINGS_CHANGE_FAILED_MSG: 'SETTINGS_CHANGE_FAILED_MSG',
RINKEBY_VERSION_MSG: 'RINKEBY_VERSION_MSG',
WRONG_NETWORK_MSG: 'WRONG_NETWORK_MSG',
ADDRESS_BOOK_NEW_ENTRY_SUCCESS: 'ADDRESS_BOOK_NEW_ENTRY_SUCCESS',
ADDRESS_BOOK_EDIT_ENTRY_SUCCESS: 'ADDRESS_BOOK_EDIT_ENTRY_SUCCESS',
ADDRESS_BOOK_DELETE_ENTRY_SUCCESS: 'ADDRESS_BOOK_DELETE_ENTRY_SUCCESS',
SAFE_NEW_VERSION_AVAILABLE: 'SAFE_NEW_VERSION_AVAILABLE',
}
export const NOTIFICATIONS: Record<NotificationId, Notification> = {
// Wallet Connection
CONNECT_WALLET_MSG: {
message: 'Please connect wallet to continue',

View File

@ -1,15 +0,0 @@
import { createAction } from 'redux-actions'
export const ENQUEUE_SNACKBAR = 'ENQUEUE_SNACKBAR'
const addSnackbar = createAction(ENQUEUE_SNACKBAR)
const enqueueSnackbar = (notification) => (dispatch) => {
const newNotification = {
...notification,
key: notification.key || new Date().getTime(),
}
dispatch(addSnackbar(newNotification))
}
export default enqueueSnackbar

View File

@ -0,0 +1,43 @@
import React from 'react'
import { AnyAction } from 'redux'
import { ThunkAction } from 'redux-thunk'
import { createAction } from 'redux-actions'
import { IconButton } from '@material-ui/core'
import { Close as IconClose } from '@material-ui/icons'
import { Notification } from 'src/logic/notifications/notificationTypes'
import closeSnackbarAction from './closeSnackbar'
import { Dispatch } from 'src/logic/safe/store/actions/types.d'
import { AppReduxState } from 'src/store'
export const ENQUEUE_SNACKBAR = 'ENQUEUE_SNACKBAR'
const addSnackbar = createAction(ENQUEUE_SNACKBAR)
const enqueueSnackbar = (
notification: Notification,
key?: string | number,
onClick?: () => void,
): ThunkAction<string | number, AppReduxState, undefined, AnyAction> => (dispatch: Dispatch) => {
key = notification.key || new Date().getTime() + Math.random()
const newNotification = {
...notification,
key,
options: {
...notification.options,
onClick,
// eslint-disable-next-line react/display-name
action: (actionKey) => (
<IconButton onClick={() => dispatch(closeSnackbarAction({ key: actionKey }))}>
<IconClose />
</IconButton>
),
},
}
dispatch(addSnackbar(newNotification))
return key
}
export default enqueueSnackbar

View File

@ -1,13 +1,12 @@
import { push } from 'connected-react-router'
import { List, Map } from 'immutable'
import { WithSnackbarProps } from 'notistack'
import { batch } from 'react-redux'
import semverSatisfies from 'semver/functions/satisfies'
import { ThunkAction } from 'redux-thunk'
import { onboardUser } from 'src/components/ConnectButton'
import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
import { getNotificationsFromTxType, showSnackbar } from 'src/logic/notifications'
import { getNotificationsFromTxType } from 'src/logic/notifications'
import {
CALL,
getApprovalTransaction,
@ -22,6 +21,8 @@ import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses'
import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions'
import { providerSelector } from 'src/logic/wallets/store/selectors'
import { SAFELIST_ADDRESS } from 'src/routes/routes'
import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar'
import closeSnackbarAction from 'src/logic/notifications/store/actions/closeSnackbar'
import { addOrUpdateCancellationTransactions } from 'src/logic/safe/store/actions/transactions/addOrUpdateCancellationTransactions'
import { addOrUpdateTransactions } from 'src/logic/safe/store/actions/transactions/addOrUpdateTransactions'
import { removeCancellationTransaction } from 'src/logic/safe/store/actions/transactions/removeCancellationTransaction'
@ -95,7 +96,7 @@ export const storeTx = async (
}
}
interface CreateTransaction extends WithSnackbarProps {
interface CreateTransactionArgs {
navigateToTransactionsTab?: boolean
notifiedTransaction: string
operation?: number
@ -108,23 +109,22 @@ interface CreateTransaction extends WithSnackbarProps {
}
type CreateTransactionAction = ThunkAction<Promise<void>, AppReduxState, undefined, AnyAction>
type ConfirmEventHandler = (safeTxHash: string) => void
const createTransaction = ({
safeAddress,
to,
valueInWei,
txData = EMPTY_DATA,
notifiedTransaction,
enqueueSnackbar,
closeSnackbar,
txNonce,
operation = CALL,
navigateToTransactionsTab = true,
origin = null,
}: CreateTransaction): CreateTransactionAction => async (
dispatch: Dispatch,
getState: () => AppReduxState,
): Promise<void> => {
const createTransaction = (
{
safeAddress,
to,
valueInWei,
txData = EMPTY_DATA,
notifiedTransaction,
txNonce,
operation = CALL,
navigateToTransactionsTab = true,
origin = null,
}: CreateTransactionArgs,
onUserConfirm?: ConfirmEventHandler,
): CreateTransactionAction => async (dispatch: Dispatch, getState: () => AppReduxState): Promise<void> => {
const state = getState()
if (navigateToTransactionsTab) {
@ -149,7 +149,7 @@ const createTransaction = ({
)}000000000000000000000000000000000000000000000000000000000000000001`
const notificationsQueue = getNotificationsFromTxType(notifiedTransaction, origin)
const beforeExecutionKey = showSnackbar(notificationsQueue.beforeExecution, enqueueSnackbar, closeSnackbar)
const beforeExecutionKey = dispatch(enqueueSnackbar(notificationsQueue.beforeExecution))
let pendingExecutionKey
@ -179,17 +179,20 @@ const createTransaction = ({
const signature = await tryOffchainSigning({ ...txArgs, safeAddress }, hardwareWallet)
if (signature) {
closeSnackbar(beforeExecutionKey)
dispatch(closeSnackbarAction({ key: beforeExecutionKey }))
await saveTxToHistory({ ...txArgs, signature, origin })
showSnackbar(notificationsQueue.afterExecution.moreConfirmationsNeeded, enqueueSnackbar, closeSnackbar)
dispatch(enqueueSnackbar(notificationsQueue.afterExecution.moreConfirmationsNeeded))
dispatch(fetchTransactions(safeAddress))
return
}
}
const tx = isExecution ? await getExecutionTransaction(txArgs) : await getApprovalTransaction(txArgs)
const safeTxHash = generateSafeTxHash(safeAddress, txArgs)
const tx = isExecution
? await getExecutionTransaction(txArgs)
: await getApprovalTransaction(safeInstance, safeTxHash)
const sendParams: PayableTx = { from, value: 0 }
// if not set owner management tests will fail on ganache
@ -201,7 +204,7 @@ const createTransaction = ({
...txArgs,
confirmations: [], // this is used to determine if a tx is pending or not. See `calculateTransactionStatus` helper
value: txArgs.valueInWei,
safeTxHash: generateSafeTxHash(safeAddress, txArgs),
safeTxHash,
submissionDate: new Date().toISOString(),
}
const mockedTx = await mockTransaction(txToMock, safeAddress, state)
@ -209,11 +212,12 @@ const createTransaction = ({
await tx
.send(sendParams)
.once('transactionHash', async (hash) => {
onUserConfirm(safeTxHash)
try {
txHash = hash
closeSnackbar(beforeExecutionKey)
dispatch(closeSnackbarAction({ key: beforeExecutionKey }))
pendingExecutionKey = showSnackbar(notificationsQueue.pendingExecution, enqueueSnackbar, closeSnackbar)
pendingExecutionKey = dispatch(enqueueSnackbar(notificationsQueue.pendingExecution))
await Promise.all([
saveTxToHistory({ ...txArgs, txHash, origin }),
@ -233,21 +237,21 @@ const createTransaction = ({
}
})
.on('error', (error) => {
closeSnackbar(pendingExecutionKey)
dispatch(closeSnackbarAction({ key: pendingExecutionKey }))
removeTxFromStore(mockedTx, safeAddress, dispatch, state)
console.error('Tx error: ', error)
})
.then(async (receipt) => {
if (pendingExecutionKey) {
closeSnackbar(pendingExecutionKey)
dispatch(closeSnackbarAction({ key: pendingExecutionKey }))
}
showSnackbar(
isExecution
? notificationsQueue.afterExecution.noMoreConfirmationsNeeded
: notificationsQueue.afterExecution.moreConfirmationsNeeded,
enqueueSnackbar,
closeSnackbar,
dispatch(
enqueueSnackbar(
isExecution
? notificationsQueue.afterExecution.noMoreConfirmationsNeeded
: notificationsQueue.afterExecution.moreConfirmationsNeeded,
),
)
const toStoreTx = isExecution
@ -283,13 +287,13 @@ const createTransaction = ({
: notificationsQueue.afterExecutionError.message
console.error(`Error creating the TX: `, err)
closeSnackbar(beforeExecutionKey)
dispatch(closeSnackbarAction({ key: beforeExecutionKey }))
if (pendingExecutionKey) {
closeSnackbar(pendingExecutionKey)
dispatch(closeSnackbarAction({ key: pendingExecutionKey }))
}
showSnackbar(errorMsg, enqueueSnackbar, closeSnackbar)
dispatch(enqueueSnackbar(errorMsg))
const executeDataUsedSignatures = safeInstance.methods
.execTransaction(to, valueInWei, txData, operation, 0, 0, 0, ZERO_ADDRESS, ZERO_ADDRESS, sigs)

View File

@ -2,12 +2,14 @@ import { fromJS } from 'immutable'
import semverSatisfies from 'semver/functions/satisfies'
import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
import { getNotificationsFromTxType, showSnackbar } from 'src/logic/notifications'
import { getNotificationsFromTxType } from 'src/logic/notifications'
import { generateSignaturesFromTxConfirmations } from 'src/logic/safe/safeTxSigner'
import { getApprovalTransaction, getExecutionTransaction, saveTxToHistory } from 'src/logic/safe/transactions'
import { SAFE_VERSION_FOR_OFFCHAIN_SIGNATURES, tryOffchainSigning } from 'src/logic/safe/transactions/offchainSigner'
import { getCurrentSafeVersion } from 'src/logic/safe/utils/safeVersion'
import { providerSelector } from 'src/logic/wallets/store/selectors'
import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar'
import closeSnackbarAction from 'src/logic/notifications/store/actions/closeSnackbar'
import fetchSafe from 'src/logic/safe/store/actions/fetchSafe'
import fetchTransactions from 'src/logic/safe/store/actions/transactions/fetchTransactions'
import {
@ -22,15 +24,10 @@ import { makeConfirmation } from '../models/confirmation'
import { storeTx } from './createTransaction'
import { TransactionStatus } from '../models/types/transaction'
const processTransaction = ({
approveAndExecute,
closeSnackbar,
enqueueSnackbar,
notifiedTransaction,
safeAddress,
tx,
userAddress,
}) => async (dispatch, getState) => {
const processTransaction = ({ approveAndExecute, notifiedTransaction, safeAddress, tx, userAddress }) => async (
dispatch,
getState,
) => {
const state = getState()
const { account: from, hardwareWallet, smartContractWallet } = providerSelector(state)
@ -51,7 +48,7 @@ const processTransaction = ({
}
const notificationsQueue = getNotificationsFromTxType(notifiedTransaction, tx.origin)
const beforeExecutionKey = showSnackbar(notificationsQueue.beforeExecution, enqueueSnackbar, closeSnackbar)
const beforeExecutionKey = dispatch(enqueueSnackbar(notificationsQueue.beforeExecution))
let pendingExecutionKey
let txHash
@ -85,17 +82,19 @@ const processTransaction = ({
const signature = await tryOffchainSigning({ ...txArgs, safeAddress }, hardwareWallet)
if (signature) {
closeSnackbar(beforeExecutionKey)
dispatch(closeSnackbarAction(beforeExecutionKey))
await saveTxToHistory({ ...txArgs, signature })
showSnackbar(notificationsQueue.afterExecution.moreConfirmationsNeeded, enqueueSnackbar, closeSnackbar)
dispatch(enqueueSnackbar(notificationsQueue.afterExecution.moreConfirmationsNeeded))
dispatch(fetchTransactions(safeAddress))
return
}
}
transaction = isExecution ? await getExecutionTransaction(txArgs) : await getApprovalTransaction(txArgs)
transaction = isExecution
? await getExecutionTransaction(txArgs)
: await getApprovalTransaction(safeInstance, tx.safeTxHash)
const sendParams: any = { from, value: 0 }
@ -114,11 +113,11 @@ const processTransaction = ({
await transaction
.send(sendParams)
.once('transactionHash', async (hash) => {
.once('transactionHash', async (hash: string) => {
txHash = hash
closeSnackbar(beforeExecutionKey)
dispatch(closeSnackbarAction(beforeExecutionKey))
pendingExecutionKey = showSnackbar(notificationsQueue.pendingExecution, enqueueSnackbar, closeSnackbar)
pendingExecutionKey = dispatch(enqueueSnackbar(notificationsQueue.pendingExecution))
try {
await Promise.all([
@ -135,27 +134,27 @@ const processTransaction = ({
])
dispatch(fetchTransactions(safeAddress))
} catch (e) {
closeSnackbar(pendingExecutionKey)
dispatch(closeSnackbarAction(pendingExecutionKey))
await storeTx(tx, safeAddress, dispatch, state)
console.error(e)
}
})
.on('error', (error) => {
closeSnackbar(pendingExecutionKey)
dispatch(closeSnackbarAction(pendingExecutionKey))
storeTx(tx, safeAddress, dispatch, state)
console.error('Processing transaction error: ', error)
})
.then(async (receipt) => {
if (pendingExecutionKey) {
closeSnackbar(pendingExecutionKey)
dispatch(closeSnackbarAction(pendingExecutionKey))
}
showSnackbar(
isExecution
? notificationsQueue.afterExecution.noMoreConfirmationsNeeded
: notificationsQueue.afterExecution.moreConfirmationsNeeded,
enqueueSnackbar,
closeSnackbar,
dispatch(
enqueueSnackbar(
isExecution
? notificationsQueue.afterExecution.noMoreConfirmationsNeeded
: notificationsQueue.afterExecution.moreConfirmationsNeeded,
),
)
const toStoreTx = isExecution
@ -207,13 +206,13 @@ const processTransaction = ({
console.error(err)
if (txHash !== undefined) {
closeSnackbar(beforeExecutionKey)
dispatch(closeSnackbarAction(beforeExecutionKey))
if (pendingExecutionKey) {
closeSnackbar(pendingExecutionKey)
dispatch(closeSnackbarAction(pendingExecutionKey))
}
showSnackbar(errorMsg, enqueueSnackbar, closeSnackbar)
dispatch(enqueueSnackbar(errorMsg))
const executeData = safeInstance.methods.approveHash(txHash).encodeABI()
const errMsg = await getErrorMessage(safeInstance.options.address, 0, executeData, from)

View File

@ -0,0 +1,38 @@
import { Transaction } from '@gnosis.pm/safe-apps-sdk'
import { AbiItem } from 'web3-utils'
import { MultiSend } from 'src/types/contracts/MultiSend.d'
import { getWeb3 } from 'src/logic/wallets/getWeb3'
import { MULTI_SEND_ADDRESS } from 'src/logic/contracts/safeContracts'
const multiSendAbi: AbiItem[] = [
{
type: 'function',
name: 'multiSend',
constant: false,
payable: false,
stateMutability: 'nonpayable',
inputs: [{ type: 'bytes', name: 'transactions' }],
outputs: [],
},
]
export const encodeMultiSendCall = (txs: Transaction[]): string => {
const web3 = getWeb3()
const multiSend = (new web3.eth.Contract(multiSendAbi, MULTI_SEND_ADDRESS) as unknown) as MultiSend
const joinedTxs = txs
.map((tx) =>
[
web3.eth.abi.encodeParameter('uint8', 0).slice(-2),
web3.eth.abi.encodeParameter('address', tx.to).slice(-40),
web3.eth.abi.encodeParameter('uint256', tx.value).slice(-64),
web3.eth.abi.encodeParameter('uint256', web3.utils.hexToBytes(tx.data).length).slice(-64),
tx.data.replace(/^0x/, ''),
].join(''),
)
.join('')
const encodedMultiSendCallData = multiSend.methods.multiSend(`0x${joinedTxs}`).encodeABI()
return encodedMultiSendCallData
}

View File

@ -1,12 +1,13 @@
import { NonPayableTransactionObject } from 'src/types/contracts/types.d'
import { TxArgs } from 'src/logic/safe/store/models/types/transaction'
import { GnosisSafe } from 'src/types/contracts/GnosisSafe'
export const CALL = 0
export const DELEGATE_CALL = 1
export const TX_TYPE_EXECUTION = 'execution'
export const TX_TYPE_CONFIRMATION = 'confirmation'
export const getApprovalTransaction = async ({
export const getTransactionHash = async ({
baseGas,
data,
gasPrice,
@ -19,13 +20,20 @@ export const getApprovalTransaction = async ({
sender,
to,
valueInWei,
}: TxArgs): Promise<NonPayableTransactionObject<void>> => {
}: TxArgs): Promise<string> => {
const txHash = await safeInstance.methods
.getTransactionHash(to, valueInWei, data, operation, safeTxGas, baseGas, gasPrice, gasToken, refundReceiver, nonce)
.call({
from: sender,
})
return txHash
}
export const getApprovalTransaction = async (
safeInstance: GnosisSafe,
txHash: string,
): Promise<NonPayableTransactionObject<void>> => {
try {
return safeInstance.methods.approveHash(txHash)
} catch (err) {

View File

@ -1,3 +1,4 @@
import { Dispatch } from 'src/logic/safe/store/actions/types.d'
import { createAction } from 'redux-actions'
import { onboard } from 'src/components/ConnectButton'
@ -9,9 +10,10 @@ export const REMOVE_PROVIDER = 'REMOVE_PROVIDER'
const removeProvider = createAction(REMOVE_PROVIDER)
export default () => (dispatch) => {
export default () => (dispatch: Dispatch): void => {
onboard.walletReset()
resetWeb3()
dispatch(removeProvider())
dispatch(
enqueueSnackbar(

View File

@ -1,6 +1,6 @@
import { Icon, ModalFooterConfirmation, Text, Title } from '@gnosis.pm/safe-react-components'
import { Transaction } from '@gnosis.pm/safe-apps-sdk'
import React from 'react'
import { Icon, Text, Title, GenericModal, ModalFooterConfirmation } from '@gnosis.pm/safe-react-components'
import { Transaction } from '@gnosis.pm/safe-apps-sdk'
import styled from 'styled-components'
import AddressInfo from 'src/components/AddressInfo'
@ -13,8 +13,26 @@ import Bold from 'src/components/layout/Bold'
import Heading from 'src/components/layout/Heading'
import Img from 'src/components/layout/Img'
import { getEthAsToken } from 'src/logic/tokens/utils/tokenHelpers'
import { OpenModalArgs } from 'src/routes/safe/components/Layout/interfaces'
import { SafeApp } from 'src/routes/safe/components/Apps/types.d'
import { humanReadableValue } from 'src/logic/tokens/utils/humanReadableValue'
import { useDispatch } from 'react-redux'
import createTransaction from 'src/logic/safe/store/actions/createTransaction'
import { MULTI_SEND_ADDRESS } from 'src/logic/contracts/safeContracts'
import { DELEGATE_CALL, TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
import { encodeMultiSendCall } from 'src/logic/safe/transactions/multisend'
const isTxValid = (t: Transaction): boolean => {
if (!['string', 'number'].includes(typeof t.value)) {
return false
}
if (typeof t.value === 'string' && !/^(0x)?[0-9a-f]+$/i.test(t.value)) {
return false
}
const isAddressValid = mustBeEthereumAddress(t.to) === undefined
return isAddressValid && t.data && typeof t.data === 'string'
}
const Wrapper = styled.div`
margin-bottom: 15px;
@ -44,33 +62,61 @@ const StyledTextBox = styled(TextBox)`
max-width: 444px;
`
const isTxValid = (t: Transaction): boolean => {
if (!['string', 'number'].includes(typeof t.value)) {
return false
}
if (typeof t.value === 'string' && !/^(0x)?[0-9a-f]+$/i.test(t.value)) {
return false
}
const isAddressValid = mustBeEthereumAddress(t.to) === undefined
return isAddressValid && t.data && typeof t.data === 'string'
type OwnProps = {
isOpen: boolean
app: SafeApp
txs: Transaction[]
safeAddress: string
safeName: string
ethBalance: string
onCancel: () => void
onUserConfirm: (safeTxHash: string) => void
onClose: () => void
}
const confirmTransactions = (
safeAddress: string,
safeName: string,
ethBalance: string,
nameApp: string,
iconApp: string,
txs: Transaction[],
openModal: (modalInfo: OpenModalArgs) => void,
closeModal: () => void,
onConfirm: () => void,
): void => {
const areTxsMalformed = txs.some((t) => !isTxValid(t))
const ConfirmTransactionModal = ({
isOpen,
app,
txs,
safeAddress,
ethBalance,
safeName,
onCancel,
onUserConfirm,
onClose,
}: OwnProps): React.ReactElement => {
const dispatch = useDispatch()
if (!isOpen) {
return null
}
const title = <ModalTitle iconUrl={iconApp} title={nameApp} />
const handleUserConfirmation = (safeTxHash: string): void => {
onUserConfirm(safeTxHash)
onClose()
}
const confirmTransactions = async () => {
const txData = encodeMultiSendCall(txs)
await dispatch(
createTransaction(
{
safeAddress,
to: MULTI_SEND_ADDRESS,
valueInWei: '0',
txData,
operation: DELEGATE_CALL,
notifiedTransaction: TX_NOTIFICATION_TYPES.STANDARD_TX,
origin: app.id,
navigateToTransactionsTab: false,
},
handleUserConfirmation,
),
)
onClose()
}
const areTxsMalformed = txs.some((t) => !isTxValid(t))
const body = areTxsMalformed ? (
<>
@ -111,22 +157,22 @@ const confirmTransactions = (
</>
)
const footer = (
<ModalFooterConfirmation
cancelText="Cancel"
handleCancel={closeModal}
handleOk={onConfirm}
okDisabled={areTxsMalformed}
okText="Submit"
return (
<GenericModal
title={<ModalTitle title={app.name} iconUrl={app.iconUrl} />}
body={body}
footer={
<ModalFooterConfirmation
cancelText="Cancel"
handleCancel={onCancel}
handleOk={confirmTransactions}
okDisabled={areTxsMalformed}
okText="Submit"
/>
}
onClose={onClose}
/>
)
openModal({
title,
body,
footer,
onClose: closeModal,
})
}
export default confirmTransactions
export default ConfirmTransactionModal

View File

@ -1,15 +1,16 @@
import { useSnackbar } from 'notistack'
import {
InterfaceMessages,
InterfaceMessageIds,
InterfaceMessageToPayload,
SDKMessages,
SDKMessageIds,
SDKMessageToPayload,
SDK_MESSAGES,
INTERFACE_MESSAGES,
RequestId,
Transaction,
} from '@gnosis.pm/safe-apps-sdk'
import { useDispatch, useSelector } from 'react-redux'
import { useEffect, useCallback, MutableRefObject } from 'react'
import { OpenModalArgs } from 'src/routes/safe/components/Layout/interfaces'
import {
safeEthBalanceSelector,
safeNameSelector,
@ -18,23 +19,30 @@ import {
import { networkSelector } from 'src/logic/wallets/store/selectors'
import { SafeApp } from 'src/routes/safe/components/Apps/types'
import sendTransactions from '../sendTransactions'
import confirmTransactions from '../confirmTransactions'
type InterfaceMessageProps<T extends InterfaceMessageIds> = {
messageId: T
data: InterfaceMessageToPayload[T]
}
type ReturnType = {
sendMessageToIframe: <T extends keyof InterfaceMessages>(messageId: T, data: InterfaceMessageToPayload[T]) => void
sendMessageToIframe: <T extends InterfaceMessageIds>(message: InterfaceMessageProps<T>, requestId?: RequestId) => void
}
interface CustomMessageEvent extends MessageEvent {
data: {
messageId: keyof SDKMessages
data: SDKMessageToPayload[keyof SDKMessages]
requestId: RequestId
messageId: SDKMessageIds
data: SDKMessageToPayload[SDKMessageIds]
}
}
interface InterfaceMessageRequest extends InterfaceMessageProps<InterfaceMessageIds> {
requestId: number | string
}
const useIframeMessageHandler = (
selectedApp: SafeApp | undefined,
openModal: (modal: OpenModalArgs) => void,
openConfirmationModal: (txs: Transaction[], requestId: RequestId) => void,
closeModal: () => void,
iframeRef: MutableRefObject<HTMLIFrameElement>,
): ReturnType => {
@ -46,9 +54,14 @@ const useIframeMessageHandler = (
const dispatch = useDispatch()
const sendMessageToIframe = useCallback(
function <T extends keyof InterfaceMessages>(messageId: T, data: InterfaceMessageToPayload[T]) {
function <T extends InterfaceMessageIds>(message: InterfaceMessageProps<T>, requestId?: RequestId) {
const requestWithMessage = {
...message,
requestId: requestId || Math.trunc(window.performance.now()),
}
if (iframeRef?.current && selectedApp) {
iframeRef.current.contentWindow.postMessage({ messageId, data }, selectedApp.url)
iframeRef.current.contentWindow.postMessage(requestWithMessage, selectedApp.url)
}
},
[iframeRef, selectedApp],
@ -60,32 +73,25 @@ const useIframeMessageHandler = (
console.error('ThirdPartyApp: A message was received without message id.')
return
}
const { requestId } = msg.data
switch (msg.data.messageId) {
case SDK_MESSAGES.SEND_TRANSACTIONS: {
const onConfirm = async () => {
closeModal()
await sendTransactions(dispatch, safeAddress, msg.data.data, enqueueSnackbar, closeSnackbar, selectedApp.id)
}
confirmTransactions(
safeAddress,
safeName,
ethBalance,
selectedApp.name,
selectedApp.iconUrl,
msg.data.data,
openModal,
closeModal,
onConfirm,
)
openConfirmationModal(msg.data.data, requestId)
break
}
case SDK_MESSAGES.SAFE_APP_SDK_INITIALIZED: {
sendMessageToIframe(INTERFACE_MESSAGES.ON_SAFE_INFO, {
safeAddress,
network: network,
ethBalance,
})
const message = {
messageId: INTERFACE_MESSAGES.ON_SAFE_INFO,
data: {
safeAddress,
network: network,
ethBalance,
},
}
sendMessageToIframe(message)
break
}
default: {
@ -116,7 +122,7 @@ const useIframeMessageHandler = (
enqueueSnackbar,
ethBalance,
network,
openModal,
openConfirmationModal,
safeAddress,
safeName,
selectedApp,

View File

@ -1,5 +1,5 @@
import React, { useCallback, useEffect, useRef, useState, useMemo } from 'react'
import { Networks, INTERFACE_MESSAGES } from '@gnosis.pm/safe-apps-sdk'
import { INTERFACE_MESSAGES, Transaction, RequestId } from '@gnosis.pm/safe-apps-sdk'
import { Card, IconText, Loader, Menu, Title } from '@gnosis.pm/safe-react-components'
import { useSelector } from 'react-redux'
import styled, { css } from 'styled-components'
@ -7,14 +7,18 @@ import styled, { css } from 'styled-components'
import ManageApps from './components/ManageApps'
import AppFrame from './components/AppFrame'
import { useAppList } from './hooks/useAppList'
import { OpenModalArgs } from 'src/routes/safe/components/Layout/interfaces'
import LCL from 'src/components/ListContentLayout'
import { networkSelector } from 'src/logic/wallets/store/selectors'
import { grantedSelector } from 'src/routes/safe/container/selector'
import { safeEthBalanceSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
import {
safeEthBalanceSelector,
safeParamAddressFromStateSelector,
safeNameSelector,
} from 'src/logic/safe/store/selectors'
import { isSameURL } from 'src/utils/url'
import { useIframeMessageHandler } from './hooks/useIframeMessageHandler'
import ConfirmTransactionModal from './components/ConfirmTransactionModal'
const centerCSS = css`
display: flex;
@ -38,26 +42,62 @@ const CenteredMT = styled.div`
margin-top: 5px;
`
type AppsProps = {
closeModal: () => void
openModal: (modal: OpenModalArgs) => void
type ConfirmTransactionModalState = {
isOpen: boolean
txs: Transaction[]
requestId: RequestId | undefined
}
const Apps = ({ closeModal, openModal }: AppsProps): React.ReactElement => {
const INITIAL_CONFIRM_TX_MODAL_STATE: ConfirmTransactionModalState = {
isOpen: false,
txs: [],
requestId: undefined,
}
const Apps = (): React.ReactElement => {
const { appList, loadingAppList, onAppToggle, onAppAdded, onAppRemoved } = useAppList()
const [appIsLoading, setAppIsLoading] = useState<boolean>(true)
const [selectedAppId, setSelectedAppId] = useState<string>()
const [confirmTransactionModal, setConfirmTransactionModal] = useState<ConfirmTransactionModalState>(
INITIAL_CONFIRM_TX_MODAL_STATE,
)
const iframeRef = useRef<HTMLIFrameElement>()
const granted = useSelector(grantedSelector)
const safeAddress = useSelector(safeParamAddressFromStateSelector)
const safeName = useSelector(safeNameSelector)
const network = useSelector(networkSelector)
const ethBalance = useSelector(safeEthBalanceSelector)
const openConfirmationModal = useCallback(
(txs: Transaction[], requestId: RequestId) =>
setConfirmTransactionModal({
isOpen: true,
txs,
requestId,
}),
[setConfirmTransactionModal],
)
const closeConfirmationModal = useCallback(() => setConfirmTransactionModal(INITIAL_CONFIRM_TX_MODAL_STATE), [
setConfirmTransactionModal,
])
const selectedApp = useMemo(() => appList.find((app) => app.id === selectedAppId), [appList, selectedAppId])
const enabledApps = useMemo(() => appList.filter((a) => !a.disabled), [appList])
const { sendMessageToIframe } = useIframeMessageHandler(selectedApp, openModal, closeModal, iframeRef)
const { sendMessageToIframe } = useIframeMessageHandler(
selectedApp,
openConfirmationModal,
closeConfirmationModal,
iframeRef,
)
const onUserTxConfirm = (safeTxHash: string) => {
sendMessageToIframe(
{ messageId: INTERFACE_MESSAGES.TRANSACTION_CONFIRMED, data: { safeTxHash } },
confirmTransactionModal.requestId,
)
}
const onSelectApp = useCallback(
(appId) => {
@ -93,10 +133,13 @@ const Apps = ({ closeModal, openModal }: AppsProps): React.ReactElement => {
}
setAppIsLoading(false)
sendMessageToIframe(INTERFACE_MESSAGES.ON_SAFE_INFO, {
safeAddress,
network: network as Networks,
ethBalance,
sendMessageToIframe({
messageId: INTERFACE_MESSAGES.ON_SAFE_INFO,
data: {
safeAddress,
network,
ethBalance,
},
})
}, [ethBalance, network, safeAddress, selectedApp, sendMessageToIframe])
@ -144,6 +187,17 @@ const Apps = ({ closeModal, openModal }: AppsProps): React.ReactElement => {
textSize="sm"
/>
</CenteredMT>
<ConfirmTransactionModal
isOpen={confirmTransactionModal.isOpen}
app={selectedApp}
safeAddress={safeAddress}
ethBalance={ethBalance}
safeName={safeName}
txs={confirmTransactionModal.txs}
onCancel={closeConfirmationModal}
onClose={closeConfirmationModal}
onUserConfirm={onUserTxConfirm}
/>
</>
)
}

View File

@ -1,51 +0,0 @@
import { DELEGATE_CALL } from 'src/logic/safe/transactions/send'
import { getWeb3 } from 'src/logic/wallets/getWeb3'
import createTransaction from 'src/logic/safe/store/actions/createTransaction'
import { MULTI_SEND_ADDRESS } from 'src/logic/contracts/safeContracts'
const multiSendAbi = [
{
type: 'function',
name: 'multiSend',
constant: false,
payable: false,
stateMutability: 'nonpayable',
inputs: [{ type: 'bytes', name: 'transactions' }],
outputs: [],
},
]
const sendTransactions = (dispatch, safeAddress, txs, enqueueSnackbar, closeSnackbar, origin) => {
const web3 = getWeb3()
const multiSend: any = new web3.eth.Contract(multiSendAbi as any, MULTI_SEND_ADDRESS)
const joinedTxs = txs
.map((tx) =>
[
web3.eth.abi.encodeParameter('uint8', 0).slice(-2),
web3.eth.abi.encodeParameter('address', tx.to).slice(-40),
web3.eth.abi.encodeParameter('uint256', tx.value).slice(-64),
web3.eth.abi.encodeParameter('uint256', web3.utils.hexToBytes(tx.data).length).slice(-64),
tx.data.replace(/^0x/, ''),
].join(''),
)
.join('')
const encodeMultiSendCallData = multiSend.methods.multiSend(`0x${joinedTxs}`).encodeABI()
return dispatch(
createTransaction({
safeAddress,
to: MULTI_SEND_ADDRESS,
valueInWei: '0',
txData: encodeMultiSendCallData,
notifiedTransaction: 'STANDARD_TX',
enqueueSnackbar,
closeSnackbar,
operation: DELEGATE_CALL,
// navigateToTransactionsTab: false,
origin,
} as any),
)
}
export default sendTransactions

View File

@ -1,13 +1,11 @@
import { GenericModal } from '@gnosis.pm/safe-react-components'
import { makeStyles } from '@material-ui/core/styles'
import React, { useState } from 'react'
import React from 'react'
import { useSelector } from 'react-redux'
import { Redirect, Route, Switch, useRouteMatch } from 'react-router-dom'
import Receive from '../Balances/Receive'
import { styles } from './style'
import { ModalState, OpenModalArgs } from './interfaces'
import Modal from 'src/components/Modal'
import NoSafe from 'src/components/NoSafe'
@ -45,36 +43,12 @@ const Layout = (props: Props): React.ReactElement => {
const { hideSendFunds, onHide, onShow, sendFunds, showReceive, showSendFunds } = props
const match = useRouteMatch()
const [modal, setModal] = useState<ModalState>({
isOpen: false,
title: '',
body: null,
footer: null,
onClose: null,
})
const safeAddress = useSelector(safeParamAddressFromStateSelector)
const provider = useSelector(providerNameSelector)
if (!safeAddress) {
return <NoSafe provider={provider} text="Safe not found" />
}
const openGenericModal = (modalConfig: OpenModalArgs): void => {
setModal({ ...modalConfig, isOpen: true })
}
const closeGenericModal = (): void => {
modal.onClose?.()
setModal({
isOpen: false,
title: null,
body: null,
footer: null,
onClose: null,
})
}
return (
<>
<LayoutHeader onShow={onShow} showSendFunds={showSendFunds} />
@ -83,15 +57,10 @@ const Layout = (props: Props): React.ReactElement => {
<Switch>
<Route exact path={`${match.path}/balances/:assetType?`} render={() => wrapInSuspense(<Balances />, null)} />
<Route exact path={`${match.path}/transactions`} render={() => wrapInSuspense(<TxsTable />, null)} />
<Route exact path={`${match.path}/apps`} render={() => wrapInSuspense(<Apps />, null)} />
{process.env.REACT_APP_NEW_TX_TAB === 'enabled' && (
<Route exact path={`${match.path}/all-transactions`} render={() => wrapInSuspense(<Transactions />, null)} />
)}
<Route
exact
path={`${match.path}/apps`}
render={() => wrapInSuspense(<Apps closeModal={closeGenericModal} openModal={openGenericModal} />, null)}
/>
<Route exact path={`${match.path}/settings`} render={() => wrapInSuspense(<Settings />, null)} />
<Route exact path={`${match.path}/address-book`} render={() => wrapInSuspense(<AddressBookTable />, null)} />
<Redirect to={`${match.path}/balances`} />
@ -111,8 +80,6 @@ const Layout = (props: Props): React.ReactElement => {
>
<Receive onClose={onHide('Receive')} />
</Modal>
{modal.isOpen && <GenericModal {...modal} onClose={closeGenericModal} />}
</>
)
}

View File

@ -1,14 +0,0 @@
export interface ModalState {
isOpen: boolean
title: string | React.ReactElement
body: React.ReactNode | null
footer: React.ReactNode | null
onClose: () => unknown
}
export interface OpenModalArgs {
title: string | React.ReactElement
body: React.ReactNode | null
footer: React.ReactNode | null
onClose: () => unknown
}

View File

@ -4,7 +4,6 @@ import { makeStyles } from '@material-ui/core/styles'
import Close from '@material-ui/icons/Close'
import OpenInNew from '@material-ui/icons/OpenInNew'
import cn from 'classnames'
import { useSnackbar } from 'notistack'
import React from 'react'
import { useDispatch, useSelector } from 'react-redux'
import styled from 'styled-components'
@ -48,8 +47,6 @@ const RemoveModuleModal = ({ onClose, selectedModule }: RemoveModuleModal): Reac
const classes = useStyles()
const safeAddress = useSelector(safeParamAddressFromStateSelector)
const { enqueueSnackbar, closeSnackbar } = useSnackbar()
const dispatch = useDispatch()
const removeSelectedModule = async (): Promise<void> => {
@ -65,8 +62,6 @@ const RemoveModuleModal = ({ onClose, selectedModule }: RemoveModuleModal): Reac
valueInWei: '0',
txData,
notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX,
enqueueSnackbar,
closeSnackbar,
}),
)
} catch (e) {

View File

@ -1,7 +1,6 @@
import IconButton from '@material-ui/core/IconButton'
import { withStyles } from '@material-ui/core/styles'
import { makeStyles } from '@material-ui/core/styles'
import Close from '@material-ui/icons/Close'
import { withSnackbar } from 'notistack'
import React from 'react'
import { useDispatch, useSelector } from 'react-redux'
@ -22,32 +21,34 @@ import Paragraph from 'src/components/layout/Paragraph'
import Row from 'src/components/layout/Row'
import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
import { updateAddressBookEntry } from 'src/logic/addressBook/store/actions/updateAddressBookEntry'
import { getNotificationsFromTxType, showSnackbar } from 'src/logic/notifications'
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
import { NOTIFICATIONS } from 'src/logic/notifications'
import editSafeOwner from 'src/logic/safe/store/actions/editSafeOwner'
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
import { sm } from 'src/theme/variables'
import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar'
export const RENAME_OWNER_INPUT_TEST_ID = 'rename-owner-input'
export const SAVE_OWNER_CHANGES_BTN_TEST_ID = 'save-owner-changes-btn'
const EditOwnerComponent = ({
classes,
closeSnackbar,
enqueueSnackbar,
isOpen,
onClose,
ownerAddress,
selectedOwnerName,
}) => {
const useStyles = makeStyles(styles)
type OwnProps = {
isOpen: true
onClose: () => void
ownerAddress: string
selectedOwnerName: string
}
const EditOwnerComponent = ({ isOpen, onClose, ownerAddress, selectedOwnerName }: OwnProps): React.ReactElement => {
const classes = useStyles()
const dispatch = useDispatch()
const safeAddress = useSelector(safeParamAddressFromStateSelector)
const handleSubmit = (values) => {
const { ownerName } = values
dispatch(editSafeOwner({ safeAddress, ownerAddress, ownerName }))
dispatch(updateAddressBookEntry(makeAddressBookEntry({ address: ownerAddress, name: ownerName })))
const notification = getNotificationsFromTxType(TX_NOTIFICATION_TYPES.OWNER_NAME_CHANGE_TX)
showSnackbar(notification.afterExecution.noMoreConfirmationsNeeded, enqueueSnackbar, closeSnackbar)
dispatch(enqueueSnackbar(NOTIFICATIONS.OWNER_NAME_CHANGE_EXECUTED_MSG))
onClose()
}
@ -75,7 +76,6 @@ const EditOwnerComponent = ({
<Block className={classes.container}>
<Row margin="md">
<Field
className={classes.addressInput}
component={TextField}
initialValue={selectedOwnerName}
name="ownerName"
@ -87,7 +87,7 @@ const EditOwnerComponent = ({
/>
</Row>
<Row>
<Block className={classes.user} justify="center">
<Block justify="center">
<Identicon address={ownerAddress} diameter={32} />
<Paragraph color="disabled" noMargin size="md" style={{ marginLeft: sm, marginRight: sm }}>
{ownerAddress}
@ -120,4 +120,4 @@ const EditOwnerComponent = ({
)
}
export default withStyles(styles as any)(withSnackbar(EditOwnerComponent))
export default EditOwnerComponent

View File

@ -1,6 +1,7 @@
import { error, lg, md, sm } from 'src/theme/variables'
import { createStyles } from '@material-ui/core'
export const styles = () => ({
export const styles = createStyles({
heading: {
padding: lg,
justifyContent: 'space-between',

View File

@ -1,6 +1,4 @@
import { makeStyles } from '@material-ui/core/styles'
import cn from 'classnames'
import { withSnackbar } from 'notistack'
import React from 'react'
import { useDispatch, useSelector } from 'react-redux'
@ -17,7 +15,8 @@ import Col from 'src/components/layout/Col'
import Heading from 'src/components/layout/Heading'
import Paragraph from 'src/components/layout/Paragraph'
import Row from 'src/components/layout/Row'
import { getNotificationsFromTxType, showSnackbar } from 'src/logic/notifications'
import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar'
import { getNotificationsFromTxType, enhanceSnackbarForAction } from 'src/logic/notifications'
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
import UpdateSafeModal from 'src/routes/safe/components/Settings/UpdateSafeModal'
import { grantedSelector } from 'src/routes/safe/container/selector'
@ -35,9 +34,9 @@ export const SAFE_NAME_INPUT_TEST_ID = 'safe-name-input'
export const SAFE_NAME_SUBMIT_BTN_TEST_ID = 'change-safe-name-btn'
export const SAFE_NAME_UPDATE_SAFE_BTN_TEST_ID = 'update-safe-name-btn'
const useStyles = makeStyles(styles as any)
const useStyles = makeStyles(styles)
const SafeDetails = (props) => {
const SafeDetails = (): React.ReactElement => {
const classes = useStyles()
const isUserOwner = useSelector(grantedSelector)
const latestMasterContractVersion = useSelector(latestMasterContractVersionSelector)
@ -45,7 +44,6 @@ const SafeDetails = (props) => {
const safeName = useSelector(safeNameSelector)
const safeNeedsUpdate = useSelector(safeNeedsUpdateSelector)
const safeCurrentVersion = useSelector(safeCurrentVersionSelector)
const { closeSnackbar, enqueueSnackbar } = props
const [isModalOpen, setModalOpen] = React.useState(false)
const safeAddress = useSelector(safeParamAddressFromStateSelector)
@ -58,7 +56,7 @@ const SafeDetails = (props) => {
dispatch(updateSafe({ address: safeAddress, name: values.safeName }))
const notification = getNotificationsFromTxType(TX_NOTIFICATION_TYPES.SAFE_NAME_CHANGE_TX)
showSnackbar(notification.afterExecution.noMoreConfirmationsNeeded, enqueueSnackbar, closeSnackbar)
dispatch(enqueueSnackbar(enhanceSnackbarForAction(notification.afterExecution.noMoreConfirmationsNeeded)))
}
const handleUpdateSafe = () => {
@ -75,7 +73,7 @@ const SafeDetails = (props) => {
<Row align="end" grow>
<Paragraph className={classes.versionNumber}>
<Link
className={cn(classes.item, classes.link)}
className={classes.link}
color="black"
target="_blank"
to="https://github.com/gnosis/safe-contracts/releases"
@ -145,4 +143,4 @@ const SafeDetails = (props) => {
)
}
export default withSnackbar(SafeDetails)
export default SafeDetails

View File

@ -1,6 +1,7 @@
import { createStyles } from '@material-ui/core/styles'
import { boldFont, border, lg, sm, connected } from 'src/theme/variables'
export const styles = () => ({
export const styles = createStyles({
formContainer: {
padding: lg,
},

View File

@ -1,9 +1,8 @@
import Checkbox from '@material-ui/core/Checkbox'
import FormControlLabel from '@material-ui/core/FormControlLabel'
import IconButton from '@material-ui/core/IconButton'
import { withStyles } from '@material-ui/core/styles'
import { makeStyles } from '@material-ui/core/styles'
import Close from '@material-ui/icons/Close'
import { withSnackbar } from 'notistack'
import React, { useEffect, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
@ -24,6 +23,9 @@ import { userAccountSelector } from 'src/logic/wallets/store/selectors'
import processTransaction from 'src/logic/safe/store/actions/processTransaction'
import { safeParamAddressFromStateSelector, safeThresholdSelector } from 'src/logic/safe/store/selectors'
import { Transaction } from 'src/logic/safe/store/models/types/transaction'
const useStyles = makeStyles(styles)
export const APPROVE_TX_MODAL_SUBMIT_BTN_TEST_ID = 'approve-tx-modal-submit-btn'
export const REJECT_TX_MODAL_SUBMIT_BTN_TEST_ID = 'reject-tx-modal-submit-btn'
@ -51,19 +53,26 @@ const getModalTitleAndDescription = (thresholdReached, isCancelTx) => {
return modalInfo
}
type Props = {
canExecute: boolean
isCancelTx?: boolean
isOpen: boolean
onClose: () => void
thresholdReached: boolean
tx: Transaction
}
const ApproveTxModal = ({
canExecute,
classes,
closeSnackbar,
enqueueSnackbar,
isCancelTx,
isCancelTx = false,
isOpen,
onClose,
thresholdReached,
tx,
}: any) => {
}: Props): React.ReactElement => {
const dispatch = useDispatch()
const userAddress = useSelector(userAccountSelector)
const classes = useStyles()
const threshold = useSelector(safeThresholdSelector)
const safeAddress = useSelector(safeParamAddressFromStateSelector)
const [approveAndExecute, setApproveAndExecute] = useState(canExecute)
@ -109,8 +118,6 @@ const ApproveTxModal = ({
tx,
userAddress,
notifiedTransaction: TX_NOTIFICATION_TYPES.CONFIRMATION_TX,
enqueueSnackbar,
closeSnackbar,
approveAndExecute: canExecute && approveAndExecute && isTheTxReadyToBeExecuted,
}),
)
@ -181,4 +188,4 @@ const ApproveTxModal = ({
)
}
export default withStyles(styles as any)(withSnackbar(ApproveTxModal))
export default ApproveTxModal

View File

@ -1,6 +1,7 @@
import { createStyles } from '@material-ui/core'
import { border, lg, md, sm } from 'src/theme/variables'
export const styles = () => ({
export const styles = createStyles({
heading: {
padding: `${sm} ${lg}`,
justifyContent: 'space-between',

View File

@ -1,7 +1,6 @@
import IconButton from '@material-ui/core/IconButton'
import { withStyles } from '@material-ui/core/styles'
import { makeStyles } from '@material-ui/core/styles'
import Close from '@material-ui/icons/Close'
import { withSnackbar } from 'notistack'
import React, { useEffect, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
@ -22,11 +21,22 @@ import { getWeb3 } from 'src/logic/wallets/getWeb3'
import createTransaction from 'src/logic/safe/store/actions/createTransaction'
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
import { Transaction } from 'src/logic/safe/store/models/types/transaction'
const RejectTxModal = ({ classes, closeSnackbar, enqueueSnackbar, isOpen, onClose, tx }) => {
const useStyles = makeStyles(styles)
type Props = {
isOpen: boolean
onClose: () => void
tx: Transaction
}
const RejectTxModal = ({ isOpen, onClose, tx }: Props): React.ReactElement => {
const [gasCosts, setGasCosts] = useState('< 0.001')
const dispatch = useDispatch()
const safeAddress = useSelector(safeParamAddressFromStateSelector)
const classes = useStyles()
useEffect(() => {
let isCurrent = true
const estimateGasCosts = async () => {
@ -55,8 +65,6 @@ const RejectTxModal = ({ classes, closeSnackbar, enqueueSnackbar, isOpen, onClos
to: safeAddress,
valueInWei: '0',
notifiedTransaction: TX_NOTIFICATION_TYPES.CANCELLATION_TX,
enqueueSnackbar,
closeSnackbar,
txNonce: tx.nonce,
origin: tx.origin,
}),
@ -111,4 +119,4 @@ const RejectTxModal = ({ classes, closeSnackbar, enqueueSnackbar, isOpen, onClos
)
}
export default withStyles(styles as any)(withSnackbar(RejectTxModal))
export default RejectTxModal

View File

@ -1,6 +1,7 @@
import { createStyles } from '@material-ui/core'
import { border, lg, md, sm } from 'src/theme/variables'
export const styles = () => ({
export const styles = createStyles({
heading: {
padding: `${sm} ${lg}`,
justifyContent: 'space-between',

1545
yarn.lock

File diff suppressed because it is too large Load Diff