Skip to content
This repository has been archived by the owner on Nov 10, 2023. It is now read-only.

Developer tools #3306

Merged
merged 10 commits into from
Jan 19, 2022
126 changes: 126 additions & 0 deletions src/components/AppLayout/Sidebar/DevTools/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { ReactElement } from 'react'
import { useSelector } from 'react-redux'
import { useHistory } from 'react-router-dom'
import styled from 'styled-components'
import { fireEvent, screen, waitForElementToBeRemoved } from '@testing-library/react'
import { Button } from '@gnosis.pm/safe-react-components'
import List from '@material-ui/core/List'
import ListItem from '@material-ui/core/ListItem'
import ListItemText from '@material-ui/core/ListItemText'

import { getShortName } from 'src/config'
import { currentSafe } from 'src/logic/safe/store/selectors'
import { generatePrefixedAddressRoutes } from 'src/routes/routes'
import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses'
import { nextTransaction } from 'src/logic/safe/store/selectors/gatewayTransactions'
import { grantedSelector } from 'src/routes/safe/container/selector'

const prepareTx = async (address: string): Promise<void> => {
const newTxBtn = screen.getByText('New transaction')
fireEvent.click(newTxBtn)

const sendBtn = await screen.findByText('Send funds')
fireEvent.click(sendBtn)

const recipientInput = await screen.findByTestId('address-book-input')
fireEvent.change(recipientInput, { target: { value: address } })

const tokenInput = await screen.findByTestId('token-input')
fireEvent.change(tokenInput, { target: { value: ZERO_ADDRESS } })

const amountInput = await screen.findByPlaceholderText('Amount*')
// @TODO: Check that amount is possible
fireEvent.change(amountInput, { target: { value: '0.0001' } })

const reviewBtn = await screen.findByText('Review')
fireEvent.click(reviewBtn)

await waitForElementToBeRemoved(await screen.findByText('Estimating'), { timeout: 10000 })
}

const submitTx = async (): Promise<void> => {
const submitBtn = await screen.findByText('Submit')
fireEvent.click(submitBtn)
}

const stopExecution = async (): Promise<void> => {
const executionCheckbox = await screen.findByTestId('execute-checkbox')
fireEvent.click(executionCheckbox)
}

const createQueuedTx = async (address: string): Promise<void> => {
await prepareTx(address)
await stopExecution()
await submitTx()
}

const createExecutedTx = async (address: string): Promise<void> => {
await prepareTx(address)
await submitTx()
}

const getStatusUrl = (address: string): string => {
return `https://rimeissner.dev/safe-status-check/#/${getShortName()}:${address}`
}

const DevTools = (): ReactElement => {
const history = useHistory()
const { owners, threshold = 1, address } = useSelector(currentSafe) ?? {}
const nextTx = useSelector(nextTransaction)
const isGranted = useSelector(grantedSelector)

const { TRANSACTIONS_QUEUE, TRANSACTIONS_HISTORY } = generatePrefixedAddressRoutes({
shortName: getShortName(),
safeAddress: address,
})

return (
<>
<List dense>
<ListItem>
<ListItemText primary="Developer Tools" secondary={`Threshold: ${threshold} / ${owners?.length || 0}`} />
</ListItem>
<ListItem button>
<ListItemText onClick={() => history.push(TRANSACTIONS_QUEUE)}>Queue</ListItemText>
</ListItem>
<ListItem button>
<ListItemText onClick={() => history.push(TRANSACTIONS_HISTORY)}>History</ListItemText>
</ListItem>
<ListItem button>
<ListItemText onClick={() => window.open(getStatusUrl(address), '_blank')}>Safe Status</ListItemText>
</ListItem>
</List>
<ButtonWrapper>
<StyledButton onClick={() => createQueuedTx(address)} size="md" variant="bordered" disabled={!isGranted}>
Queue Transaction
</StyledButton>
<StyledButton
onClick={() => createExecutedTx(address)}
size="md"
variant="bordered"
disabled={!!nextTx || !isGranted}
>
Execute Transaction
</StyledButton>
</ButtonWrapper>
</>
)
}

export default DevTools

const StyledButton = styled(Button)`
&.MuiButton-root {
padding: 0 12px !important;
margin-top: 6px;
width: 100%;
}

& .MuiButton-label {
font-size: 14px !important;
}
`

const ButtonWrapper = styled.div`
margin: 0 12px;
`
8 changes: 7 additions & 1 deletion src/components/AppLayout/Sidebar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import List, { ListItemType } from 'src/components/List'
import SafeHeader from './SafeHeader'
import DebugToggle from './DebugToggle'
import { IS_PRODUCTION } from 'src/utils/constants'
import DevTools from './DevTools'

const StyledDivider = styled(Divider)`
margin: 16px -8px 0;
Expand Down Expand Up @@ -76,8 +77,13 @@ const Sidebar = ({
<List items={items} />
</>
) : null}

<HelpContainer>
{!IS_PRODUCTION && safeAddress && (
<>
<StyledDivider />
<DevTools />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would also lazy import it if possible, so that it's not imported on prod.

</>
)}
{!IS_PRODUCTION && <DebugToggle />}

<StyledDivider />
Expand Down
7 changes: 7 additions & 0 deletions src/logic/safe/store/selectors/gatewayTransactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@ export const nextTransactions = createSelector(
},
)

export const nextTransaction = createSelector(nextTransactions, (nextTxs) => {
if (!nextTxs) return

const [txs] = Object.values(nextTxs)
return txs?.[0]
})

export const queuedTransactions = createSelector(
pendingTransactions,
(pendingTransactions): StoreStructure['queued']['queued'] | undefined => {
Expand Down
53 changes: 53 additions & 0 deletions src/logic/safe/utils/mocks/remoteConfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,59 @@
"SPENDING_LIMIT"
]
},
{
"transactionService": "https://safe-transaction.xdai.gnosis.io",
"chainId": "100",
"chainName": "Gnosis Chain",
"shortName": "gno",
"l2": true,
"description": "Gnosis Chain",
"rpcUri": {
"authentication": "NO_AUTHENTICATION",
"value": "https://rpc.xdaichain.com/oe-only/"
},
"safeAppsRpcUri": {
"authentication": "NO_AUTHENTICATION",
"value": "https://rpc.xdaichain.com/oe-only/"
},
"publicRpcUri": {
"authentication": "NO_AUTHENTICATION",
"value": "https://rpc.xdaichain.com/oe-only/"
},
"blockExplorerUriTemplate": {
"address": "https://blockscout.com/xdai/mainnet/address/{{address}}/transactions",
"txHash": "https://blockscout.com/xdai/mainnet/tx/{{txHash}}/",
"api": "https://blockscout.com/poa/xdai/api?module={{module}}&action={{action}}&address={{address}}&apiKey={{apiKey}}"
},
"nativeCurrency": {
"name": "xDai",
"symbol": "XDAI",
"decimals": 18,
"logoUri": "https://safe-transaction-assets.staging.gnosisdev.com/chains/100/currency_logo.png"
},
"theme": {
"textColor": "#ffffff",
"backgroundColor": "#48A9A6"
},
"gasPrice": [
{
"type": "FIXED",
"weiValue": "4000000000"
}
],
"disabledWallets": [
"metamask",
"walletConnect"
],
"features": [
"CONTRACT_INTERACTION",
"EIP1559",
"ERC721",
"SAFE_APPS",
"SAFE_TX_GAS_OPTIONAL",
"SPENDING_LIMIT"
]
},
{
"transactionService": "https://safe-transaction-polygon.staging.gnosisdev.com",
"chainId": "137",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,14 @@ const TokenSelectField = ({ initialValue, isValid = true, tokens }: TokenSelectF
<Field
classes={{ selectMenu: classes.selectMenu }}
className={isValid ? 'isValid' : 'isInvalid'}
component={SelectField}
component={(props) => (
<SelectField
{...props}
inputProps={{
'data-testid': 'token-input',
}}
/>
)}
displayEmpty
initialValue={initialValue}
name="token"
Expand Down