Support Circles Safes in frontend on xDai (#1636)

* fix web3 issue

* update settings

* get masterCopy Address from backend

* fix prettier errors

* Fix Circles typo

* Change Circles detect logic

* Use sameAddress to compare mastercopy addresses

Co-authored-by: Daniel Sanchez <daniel.sanchez@gnosis.pm>
This commit is contained in:
nicolas
2020-11-26 18:00:19 +01:00
committed by GitHub
co-authored by Daniel Sanchez
parent e3a8b9fe2a
commit 0ecd70fdbc
18 changed files with 269 additions and 202 deletions
+3 -3
View File
@@ -1,7 +1,7 @@
!.eslintrc.js
build
config
contracts
/config
/contracts
flow-typed
flow-typed/npm
migrations
@@ -9,5 +9,5 @@ node_modules
public
scripts
src/assets
src/config
src/types/contracts
test
+2 -2
View File
@@ -80,7 +80,7 @@ describe('Config Services', () => {
jest.mock('src/utils/constants', () => ({
NODE_ENV: 'production',
NETWORK: 'MAINNET',
APP_ENV: 'production'
APP_ENV: 'production',
}))
const { getTxServiceUrl, getGnosisSafeAppsUrl } = require('src/config')
const TX_SERVICE_URL = mainnet.environment.production.txServiceUrl
@@ -100,7 +100,7 @@ describe('Config Services', () => {
jest.mock('src/utils/constants', () => ({
NODE_ENV: 'production',
NETWORK: 'XDAI',
APP_ENV: 'production'
APP_ENV: 'production',
}))
const { getTxServiceUrl, getGnosisSafeAppsUrl } = require('src/config')
const TX_SERVICE_URL = xdai.environment.production.txServiceUrl
+8 -8
View File
@@ -32,9 +32,9 @@ const getCurrentEnvironment = (): string => {
}
type NetworkSpecificConfiguration = EnvironmentSettings & {
network: NetworkSettings,
disabledFeatures?: SafeFeatures,
disabledWallets?: Wallets,
network: NetworkSettings
disabledFeatures?: SafeFeatures
disabledWallets?: Wallets
}
const configuration = (): NetworkSpecificConfiguration => {
@@ -60,7 +60,7 @@ const configuration = (): NetworkSpecificConfiguration => {
...networkBaseConfig,
network: configFile.network,
disabledFeatures: configFile.disabledFeatures,
disabledWallets: configFile.disabledWallets
disabledWallets: configFile.disabledWallets,
}
}
@@ -137,10 +137,10 @@ const fetchContractABI = memoize(
(url, contractAddress) => `${url}_${contractAddress}`,
)
const getNetworkExplorerApiKey = (networkExplorerName: string): string | undefined=> {
const getNetworkExplorerApiKey = (networkExplorerName: string): string | undefined => {
switch (networkExplorerName.toLowerCase()) {
case 'etherscan': {
return ETHERSCAN_API_KEY
return ETHERSCAN_API_KEY
}
default: {
return undefined
@@ -148,7 +148,7 @@ const getNetworkExplorerApiKey = (networkExplorerName: string): string | undefin
}
}
export const getContractABI = async (contractAddress: string) =>{
export const getContractABI = async (contractAddress: string) => {
const { apiUrl, name } = getNetworkExplorerInfo()
const apiKey = getNetworkExplorerApiKey(name)
@@ -181,7 +181,7 @@ export const getExplorerInfo = (hash: string): BlockScanInfo => {
const type = hash.length > 42 ? 'tx' : 'address'
return () => ({
url: `${url}/${type}/${hash}`,
alt: name || '',
alt: name || '',
})
}
}
@@ -41,11 +41,10 @@ describe('Networks config files test', () => {
return
}
const environmentConfigKeys = Object
.keys(networkConfigElement)
.filter((environmentConfigKey) =>
environmentConfigKey.endsWith('Uri') && !!networkConfigElement[environmentConfigKey]
)
const environmentConfigKeys = Object.keys(networkConfigElement).filter(
(environmentConfigKey) =>
environmentConfigKey.endsWith('Uri') && !!networkConfigElement[environmentConfigKey],
)
// Then
environmentConfigKeys.forEach((environmentConfigKey) => {
@@ -53,7 +52,10 @@ describe('Networks config files test', () => {
const isValid = isValidURL(networkConfigElementUri)
if (!isValid) {
console.log(`Invalid URI in "${networkFileName}" at ${environment}.${environmentConfigKey}:`, networkConfigElementUri)
console.log(
`Invalid URI in "${networkFileName}" at ${environment}.${environmentConfigKey}:`,
networkConfigElementUri,
)
}
expect(isValid).toBeTruthy()
+1 -1
View File
@@ -60,7 +60,7 @@ const mainnet: NetworkConfig = {
WALLETS.WALLET_LINK,
WALLETS.AUTHEREUM,
WALLETS.LATTICE,
]
],
}
export default mainnet
+1 -1
View File
@@ -11,5 +11,5 @@ export default {
rinkeby,
xdai,
energy_web_chain,
volta
volta,
}
+1 -1
View File
@@ -42,7 +42,7 @@ const mainnet: NetworkConfig = {
decimals: 18,
logoUri: EtherLogo,
},
}
},
}
export default mainnet
+16 -14
View File
@@ -51,12 +51,12 @@ export enum ETHEREUM_NETWORK {
export type NetworkSettings = {
// TODO: id now seems to be unnecessary
id: ETHEREUM_NETWORK,
backgroundColor: string,
textColor: string,
label: string,
isTestNet: boolean,
nativeCoin: Token,
id: ETHEREUM_NETWORK
backgroundColor: string
textColor: string
label: string
isTestNet: boolean
nativeCoin: Token
}
// something around this to display or not some critical sections in the app, depending on the network support
@@ -73,14 +73,16 @@ export type GasPriceOracle = {
gasParameter: string
}
type GasPrice = {
gasPrice: number
gasPriceOracle?: GasPriceOracle
} | {
gasPrice?: number
// for infura there's a REST API Token required stored in: `REACT_APP_INFURA_TOKEN`
gasPriceOracle: GasPriceOracle
}
type GasPrice =
| {
gasPrice: number
gasPriceOracle?: GasPriceOracle
}
| {
gasPrice?: number
// for infura there's a REST API Token required stored in: `REACT_APP_INFURA_TOKEN`
gasPriceOracle: GasPriceOracle
}
export type EnvironmentSettings = GasPrice & {
txServiceUrl: string
+2 -5
View File
@@ -14,12 +14,11 @@ const baseConfig: EnvironmentSettings = {
const xDai: NetworkConfig = {
environment: {
staging: {
...baseConfig
...baseConfig,
},
production: {
...baseConfig,
safeAppsUrl: 'https://apps-xdai.gnosis-safe.io',
},
},
network: {
@@ -52,9 +51,7 @@ const xDai: NetworkConfig = {
WALLETS.AUTHEREUM,
WALLETS.LATTICE,
],
disabledFeatures: [
FEATURES.ENS_LOOKUP,
],
disabledFeatures: [FEATURES.ENS_LOOKUP],
}
export default xDai
+47
View File
@@ -0,0 +1,47 @@
import axios from 'axios'
import { getTxServiceUrl } from 'src/config'
import memoize from 'lodash.memoize'
export enum MasterCopyDeployer {
GNOSIS = 'Gnosis',
CIRCLES = 'Circles',
}
type MasterCopyFetch = {
address: string
version: string
}
export type MasterCopy = {
address: string
version: string
deployer: MasterCopyDeployer
deployerRepoUrl: string
}
const extractMasterCopyInfo = (mc: MasterCopyFetch): MasterCopy => {
const isCircles = mc.version.toLowerCase().includes(MasterCopyDeployer.CIRCLES.toLowerCase())
const dashIndex = mc.version.indexOf('-')
const masterCopy = {
address: mc.address,
version: !isCircles ? mc.version : mc.version.substring(0, dashIndex),
deployer: !isCircles ? MasterCopyDeployer.GNOSIS : MasterCopyDeployer.CIRCLES,
deployerRepoUrl: !isCircles
? 'https://github.com/gnosis/safe-contracts/releases'
: 'https://github.com/CirclesUBI/safe-contracts/releases',
}
return masterCopy
}
export const fetchMasterCopies = memoize(
async (): Promise<MasterCopy[] | undefined> => {
const url = `${getTxServiceUrl()}/about/master-copies/`
try {
const res = await axios.get<{ address: string; version: string }[]>(url)
return res.data.map(extractMasterCopyInfo)
} catch (error) {
console.error('Fetching data from master-copies errored', error)
}
},
)
+14 -6
View File
@@ -14,25 +14,33 @@ import { AbiItem } from 'web3-utils'
*/
type MethodsArgsType = Array<string | number>
interface Props {
interface Props {
abi: AbiItem[]
address: string
batch?: BatchRequest
context?: unknown
methods: Array<string | {method: string, type?: string, args: MethodsArgsType }>
}
methods: Array<string | { method: string; type?: string; args: MethodsArgsType }>
}
const generateBatchRequests = <ReturnValues>({ abi, address, batch, context, methods }: Props): Promise<ReturnValues> => {
const generateBatchRequests = <ReturnValues>({
abi,
address,
batch,
context,
methods,
}: Props): Promise<ReturnValues> => {
const contractInstance = new web3.eth.Contract(abi, address)
const localBatch = new web3.BatchRequest()
const values = methods.map((methodObject) => {
let method, type, args: MethodsArgsType = []
let method,
type,
args: MethodsArgsType = []
if (typeof methodObject === 'string') {
method = methodObject
} else {
({ method, type, args } = methodObject)
;({ method, type, args } = methodObject)
}
return new Promise((resolve) => {
+1 -1
View File
@@ -1,4 +1,4 @@
//
//
// Code of the safe v1.0.0
const proxyCodeV10 =
+53 -49
View File
@@ -1,17 +1,16 @@
import { AbiItem } from 'web3-utils'
import GnosisSafeSol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafe.json'
import memoize from 'lodash.memoize'
import ProxyFactorySol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafeProxyFactory.json'
import SafeProxy from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafeProxy.json'
import Web3 from 'web3'
import { ETHEREUM_NETWORK } from 'src/config/networks/network.d'
import { isProxyCode } from 'src/logic/contracts/historicProxyCode'
import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses'
import { calculateGasOf, calculateGasPrice } from 'src/logic/wallets/ethTransactions'
import { getWeb3, getNetworkIdFrom } from 'src/logic/wallets/getWeb3'
import { GnosisSafe } from 'src/types/contracts/GnosisSafe.d'
import { GnosisSafeProxyFactory } from 'src/types/contracts/GnosisSafeProxyFactory.d'
import { AllowanceModule } from 'src/types/contracts/AllowanceModule.d'
import { getSafeInfo, SafeInfo } from 'src/logic/safe/utils/safeInformation'
import { SPENDING_LIMIT_MODULE_ADDRESS } from 'src/utils/constants'
import SpendingLimitModule from './artifacts/AllowanceModule.json'
@@ -22,7 +21,6 @@ export const SAFE_MASTER_COPY_ADDRESS = '0x34CfAC646f301356fAa8B21e94227e3583Fe3
export const DEFAULT_FALLBACK_HANDLER_ADDRESS = '0xd5D82B6aDDc9027B22dCA772Aa68D5d74cdBdF44'
export const SAFE_MASTER_COPY_ADDRESS_V10 = '0xb6029EA3B2c51D09a50B53CA8012FeEB05bDa35A'
let proxyFactoryMaster: GnosisSafeProxyFactory
let safeMaster: GnosisSafe
@@ -31,13 +29,13 @@ let safeMaster: GnosisSafe
* @param {Web3} web3
* @param {ETHEREUM_NETWORK} networkId
*/
const createGnosisSafeContract = (web3: Web3, networkId: ETHEREUM_NETWORK) => {
export const getGnosisSafeContract = (web3: Web3, networkId: ETHEREUM_NETWORK) => {
const networks = GnosisSafeSol.networks
// TODO: this may not be the most scalable approach,
// but up until v1.2.0 the address is the same for all the networks.
// So, if we can't find the network in the Contract artifact, we fallback to MAINNET.
const contractAddress = networks[networkId]?.address ?? networks[ETHEREUM_NETWORK.MAINNET].address
return new web3.eth.Contract(GnosisSafeSol.abi as AbiItem[], contractAddress) as unknown as GnosisSafe
return (new web3.eth.Contract(GnosisSafeSol.abi as AbiItem[], contractAddress) as unknown) as GnosisSafe
}
/**
@@ -45,48 +43,69 @@ const createGnosisSafeContract = (web3: Web3, networkId: ETHEREUM_NETWORK) => {
* @param {Web3} web3
* @param {ETHEREUM_NETWORK} networkId
*/
const createProxyFactoryContract = (web3: Web3, networkId: ETHEREUM_NETWORK): GnosisSafeProxyFactory => {
const getProxyFactoryContract = (web3: Web3, networkId: ETHEREUM_NETWORK): GnosisSafeProxyFactory => {
const networks = ProxyFactorySol.networks
// TODO: this may not be the most scalable approach,
// but up until v1.2.0 the address is the same for all the networks.
// So, if we can't find the network in the Contract artifact, we fallback to MAINNET.
const contractAddress = networks[networkId]?.address ?? networks[ETHEREUM_NETWORK.MAINNET].address
return new web3.eth.Contract(ProxyFactorySol.abi as AbiItem[], contractAddress) as unknown as GnosisSafeProxyFactory
return (new web3.eth.Contract(ProxyFactorySol.abi as AbiItem[], contractAddress) as unknown) as GnosisSafeProxyFactory
}
const createSpendingLimitContract = () => {
/**
* Creates a Contract instance of the GnosisSafeProxyFactory contract
*/
export const getSpendingLimitContract = () => {
const web3 = getWeb3()
return new web3.eth.Contract(SpendingLimitModule.abi as AbiItem[], SPENDING_LIMIT_MODULE_ADDRESS)
return (new web3.eth.Contract(
SpendingLimitModule.abi as AbiItem[],
SPENDING_LIMIT_MODULE_ADDRESS,
) as unknown) as AllowanceModule
}
export const getGnosisSafeContract = memoize(createGnosisSafeContract)
export const getSpendingLimitContract = memoize(createSpendingLimitContract)
const getCreateProxyFactoryContract = memoize(createProxyFactoryContract)
export const getMasterCopyAddressFromProxyAddress = async (proxyAddress: string): Promise<string | undefined> => {
const res = await getSafeInfo(proxyAddress)
const masterCopyAddress = (res as SafeInfo)?.masterCopy
if (!masterCopyAddress) {
console.error(`There was not possible to get masterCopy address from proxy ${proxyAddress}.`)
return
}
return masterCopyAddress
}
const instantiateMasterCopies = async () => {
export const instantiateSafeContracts = async () => {
const web3 = getWeb3()
const networkId = await getNetworkIdFrom(web3)
// Create ProxyFactory Master Copy
proxyFactoryMaster = getCreateProxyFactoryContract(web3, networkId)
proxyFactoryMaster = getProxyFactoryContract(web3, networkId)
// Create Safe Master copy
safeMaster = getGnosisSafeContract(web3, networkId)
}
export const initContracts = instantiateMasterCopies
export const getSafeMasterContract = async () => {
await initContracts()
await instantiateSafeContracts()
return safeMaster
}
export const getSafeDeploymentTransaction = (safeAccounts: string[], numConfirmations: number, safeCreationSalt: number) => {
export const getSafeDeploymentTransaction = (
safeAccounts: string[],
numConfirmations: number,
safeCreationSalt: number,
) => {
const gnosisSafeData = safeMaster.methods
.setup(safeAccounts, numConfirmations, ZERO_ADDRESS, '0x', DEFAULT_FALLBACK_HANDLER_ADDRESS, ZERO_ADDRESS, 0, ZERO_ADDRESS)
.setup(
safeAccounts,
numConfirmations,
ZERO_ADDRESS,
'0x',
DEFAULT_FALLBACK_HANDLER_ADDRESS,
ZERO_ADDRESS,
0,
ZERO_ADDRESS,
)
.encodeABI()
return proxyFactoryMaster.methods.createProxyWithNonce(safeMaster.options.address, gnosisSafeData, safeCreationSalt)
}
@@ -94,10 +113,19 @@ export const estimateGasForDeployingSafe = async (
safeAccounts: string[],
numConfirmations: number,
userAccount: string,
safeCreationSalt: number
safeCreationSalt: number,
) => {
const gnosisSafeData = await safeMaster.methods
.setup(safeAccounts, numConfirmations, ZERO_ADDRESS, '0x', DEFAULT_FALLBACK_HANDLER_ADDRESS, ZERO_ADDRESS, 0, ZERO_ADDRESS)
.setup(
safeAccounts,
numConfirmations,
ZERO_ADDRESS,
'0x',
DEFAULT_FALLBACK_HANDLER_ADDRESS,
ZERO_ADDRESS,
0,
ZERO_ADDRESS,
)
.encodeABI()
const proxyFactoryData = proxyFactoryMaster.methods
.createProxyWithNonce(safeMaster.options.address, gnosisSafeData, safeCreationSalt)
@@ -110,29 +138,5 @@ export const estimateGasForDeployingSafe = async (
export const getGnosisSafeInstanceAt = (safeAddress: string): GnosisSafe => {
const web3 = getWeb3()
return new web3.eth.Contract(GnosisSafeSol.abi as AbiItem[], safeAddress) as unknown as GnosisSafe
}
const cleanByteCodeMetadata = (bytecode: string): string => {
const metaData = 'a165'
return bytecode.substring(0, bytecode.lastIndexOf(metaData))
}
export const validateProxy = async (safeAddress: string): Promise<boolean> => {
// https://solidity.readthedocs.io/en/latest/metadata.html#usage-for-source-code-verification
const web3 = getWeb3()
const code = await web3.eth.getCode(safeAddress)
const codeWithoutMetadata = cleanByteCodeMetadata(code)
const supportedProxies = [SafeProxy]
for (let i = 0; i < supportedProxies.length; i += 1) {
const proxy = supportedProxies[i]
const proxyCode = proxy.deployedBytecode
const proxyCodeWithoutMetadata = cleanByteCodeMetadata(proxyCode)
if (codeWithoutMetadata === proxyCodeWithoutMetadata) {
return true
}
}
return isProxyCode(codeWithoutMetadata)
return (new web3.eth.Contract(GnosisSafeSol.abi as AbiItem[], safeAddress) as unknown) as GnosisSafe
}
@@ -20,10 +20,9 @@ import {
import Block from 'src/components/layout/Block'
import Col from 'src/components/layout/Col'
import Paragraph from 'src/components/layout/Paragraph'
import { SAFE_MASTER_COPY_ADDRESS_V10, getSafeMasterContract, validateProxy } from 'src/logic/contracts/safeContracts'
import { getWeb3 } from 'src/logic/wallets/getWeb3'
import { FIELD_LOAD_ADDRESS, FIELD_LOAD_NAME } from 'src/routes/load/components/fields'
import { secondary } from 'src/theme/variables'
import { getSafeInfo } from 'src/logic/safe/utils/safeInformation'
const useStyles = makeStyles({
root: {
@@ -42,42 +41,23 @@ const useStyles = makeStyles({
},
})
export const SAFE_INSTANCE_ERROR = 'Address given is not a Safe instance'
export const SAFE_MASTERCOPY_ERROR = 'Address is not a Safe or mastercopy is not supported'
export const SAFE_ADDRESS_NOT_VALID = 'Address given is not a valid Safe address'
// In case of an error here, it will be swallowed by final-form
// So if you're experiencing any strang behaviours like freeze or hanging
// Don't mind to check if everything is OK inside this function :)
export const safeFieldsValidation = async (values): Promise<Record<string, string>> => {
const errors = {}
const web3 = getWeb3()
const safeAddress = values[FIELD_LOAD_ADDRESS]
const address = values[FIELD_LOAD_ADDRESS]
if (!safeAddress || mustBeEthereumAddress(safeAddress) !== undefined) {
if (!address || mustBeEthereumAddress(address) !== undefined) {
return errors
}
const isValidProxy = await validateProxy(safeAddress)
if (!isValidProxy) {
errors[FIELD_LOAD_ADDRESS] = SAFE_INSTANCE_ERROR
return errors
}
// check mastercopy
const proxyAddressFromStorage = await web3.eth.getStorageAt(safeAddress, 0)
// https://www.reddit.com/r/ethereum/comments/6l3da1/how_long_are_ethereum_addresses/
// ganache returns plain address
// rinkeby returns 0x0000000000000+{40 address charachers}
// address comes last so we just get last 40 charachers (1byte = 2hex chars)
const checksummedProxyAddress = web3.utils.toChecksumAddress(
`0x${proxyAddressFromStorage.substr(proxyAddressFromStorage.length - 40)}`,
)
const safeMaster = await getSafeMasterContract()
const masterCopy = safeMaster.options.address
const sameMasterCopy =
checksummedProxyAddress === masterCopy || checksummedProxyAddress === SAFE_MASTER_COPY_ADDRESS_V10
if (!sameMasterCopy) {
errors[FIELD_LOAD_ADDRESS] = SAFE_MASTERCOPY_ERROR
// if getSafeInfo does not provide data, it's not a valid safe.
const safeInfo = await getSafeInfo(address)
if (!safeInfo) {
errors[FIELD_LOAD_ADDRESS] = SAFE_ADDRESS_NOT_VALID
}
return errors
+2 -2
View File
@@ -6,7 +6,7 @@ import Stepper, { StepperPage } from 'src/components/Stepper'
import Block from 'src/components/layout/Block'
import Heading from 'src/components/layout/Heading'
import Row from 'src/components/layout/Row'
import { initContracts } from 'src/logic/contracts/safeContracts'
import { instantiateSafeContracts } from 'src/logic/contracts/safeContracts'
import { Review } from 'src/routes/open/components/ReviewInformation'
import SafeNameField from 'src/routes/open/components/SafeNameForm'
import { SafeOwnersPage } from 'src/routes/open/components/SafeOwnersConfirmationsForm'
@@ -105,7 +105,7 @@ export const Layout = (props: LayoutProps): React.ReactElement => {
useEffect(() => {
if (provider) {
initContracts()
instantiateSafeContracts()
}
}, [provider])
-1
View File
@@ -62,7 +62,6 @@ export const createSafe = (values, userAccount) => {
const safeCreationSalt = getSafeCreationSaltFrom(values)
const deploymentTx = getSafeDeploymentTransaction(ownerAddresses, confirmations, safeCreationSalt)
const promiEvent = deploymentTx.send({ from: userAccount })
promiEvent
+2 -2
View File
@@ -9,7 +9,7 @@ import Button from 'src/components/layout/Button'
import Heading from 'src/components/layout/Heading'
import Img from 'src/components/layout/Img'
import Paragraph from 'src/components/layout/Paragraph'
import { initContracts } from 'src/logic/contracts/safeContracts'
import { instantiateSafeContracts } from 'src/logic/contracts/safeContracts'
import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions'
import { getWeb3 } from 'src/logic/wallets/getWeb3'
import { background, connected } from 'src/theme/variables'
@@ -152,7 +152,7 @@ const SafeDeployment = ({ creationTxHash, onCancel, onRetry, onSuccess, submitte
useEffect(() => {
const loadContracts = async () => {
await initContracts()
await instantiateSafeContracts()
setLoading(false)
}
@@ -1,5 +1,5 @@
import { makeStyles } from '@material-ui/core/styles'
import React, { useEffect } from 'react'
import React, { useEffect, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { styles } from './style'
@@ -17,6 +17,7 @@ import Paragraph from 'src/components/layout/Paragraph'
import Row from 'src/components/layout/Row'
import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar'
import { getNotificationsFromTxType, enhanceSnackbarForAction } from 'src/logic/notifications'
import { sameAddress } from 'src/logic/wallets/ethAddresses'
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'
@@ -30,6 +31,8 @@ import {
safeParamAddressFromStateSelector,
} from 'src/logic/safe/store/selectors'
import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics'
import { fetchMasterCopies, MasterCopy, MasterCopyDeployer } from 'src/logic/contracts/api/masterCopies'
import { getMasterCopyAddressFromProxyAddress } from 'src/logic/contracts/safeContracts'
export const SAFE_NAME_INPUT_TEST_ID = 'safe-name-input'
export const SAFE_NAME_SUBMIT_BTN_TEST_ID = 'change-safe-name-btn'
@@ -49,6 +52,7 @@ const SafeDetails = (): React.ReactElement => {
const [isModalOpen, setModalOpen] = React.useState(false)
const safeAddress = useSelector(safeParamAddressFromStateSelector)
const [safeInfo, setSafeInfo] = useState<MasterCopy | undefined>()
const toggleModal = () => {
setModalOpen((prevOpen) => !prevOpen)
@@ -65,87 +69,111 @@ const SafeDetails = (): React.ReactElement => {
setModalOpen(true)
}
const getSafeVersion = () => {
if (!safeInfo) {
return ''
}
return safeInfo.deployer === MasterCopyDeployer.GNOSIS
? safeCurrentVersion
: `${safeCurrentVersion}-${safeInfo.deployer}`
}
const getSafeVersionUpdate = () => {
if (!safeInfo) {
return ''
}
return safeInfo.deployer === MasterCopyDeployer.GNOSIS && safeNeedsUpdate
? ` (there's a newer version: ${latestMasterContractVersion})`
: ''
}
useEffect(() => {
trackEvent({ category: SAFE_NAVIGATION_EVENT, action: 'Settings', label: 'Details' })
}, [trackEvent])
useEffect(() => {
const getMasterCopyInfo = async () => {
const masterCopies = await fetchMasterCopies()
const masterCopyAddress = await getMasterCopyAddressFromProxyAddress(safeAddress)
const masterCopy = masterCopies?.find((mc) => sameAddress(mc.address, masterCopyAddress))
setSafeInfo(masterCopy)
}
if (safeAddress) {
getMasterCopyInfo()
}
}, [safeAddress])
return (
<>
<GnoForm onSubmit={handleSubmit}>
{() => (
<>
<Block className={classes.formContainer}>
<Heading tag="h2">Safe Version</Heading>
<GnoForm onSubmit={handleSubmit}>
{() => (
<>
<Block className={classes.formContainer}>
<Heading tag="h2">Safe Version</Heading>
<Row align="end" grow>
<Paragraph className={classes.versionNumber}>
<Link className={classes.link} color="black" target="_blank" to={safeInfo?.deployerRepoUrl}>
{getSafeVersion()}
{getSafeVersionUpdate()}
</Link>
</Paragraph>
</Row>
{safeNeedsUpdate && isUserOwner ? (
<Row align="end" grow>
<Paragraph className={classes.versionNumber}>
<Link
className={classes.link}
color="black"
target="_blank"
to="https://github.com/gnosis/safe-contracts/releases"
<Paragraph>
<Button
className={classes.saveBtn}
color="primary"
onClick={handleUpdateSafe}
size="small"
testId={SAFE_NAME_UPDATE_SAFE_BTN_TEST_ID}
variant="contained"
>
{safeCurrentVersion}
{safeNeedsUpdate && ` (there's a newer version: ${latestMasterContractVersion})`}
</Link>
Update Safe
</Button>
</Paragraph>
</Row>
{safeNeedsUpdate && isUserOwner ? (
<Row align="end" grow>
<Paragraph>
<Button
className={classes.saveBtn}
color="primary"
onClick={handleUpdateSafe}
size="small"
testId={SAFE_NAME_UPDATE_SAFE_BTN_TEST_ID}
variant="contained"
>
Update Safe
</Button>
</Paragraph>
</Row>
) : null}
) : null}
</Block>
<Block className={classes.formContainer}>
<Heading tag="h2">Modify Safe name</Heading>
<Paragraph>
You can change the name of this Safe. This name is only stored locally and never shared with Gnosis or any
third parties.
</Paragraph>
<Block className={classes.root}>
<Field
component={TextField}
defaultValue={safeName}
name="safeName"
placeholder="Safe name*"
testId={SAFE_NAME_INPUT_TEST_ID}
text="Safe name*"
type="text"
validate={composeValidators(required, minMaxLength(1, 50))}
/>
</Block>
<Block className={classes.formContainer}>
<Heading tag="h2">Modify Safe name</Heading>
<Paragraph>
You can change the name of this Safe. This name is only stored locally and never shared with Gnosis or
any third parties.
</Paragraph>
<Block className={classes.root}>
<Field
component={TextField}
defaultValue={safeName}
name="safeName"
placeholder="Safe name*"
testId={SAFE_NAME_INPUT_TEST_ID}
text="Safe name*"
type="text"
validate={composeValidators(required, minMaxLength(1, 50))}
/>
</Block>
</Block>
<Row align="end" className={classes.controlsRow} grow>
<Col end="xs">
<Button
className={classes.saveBtn}
color="primary"
size="small"
testId={SAFE_NAME_SUBMIT_BTN_TEST_ID}
type="submit"
variant="contained"
>
Save
</Button>
</Col>
</Row>
<Modal description="Update Safe" handleClose={toggleModal} open={isModalOpen} title="Update Safe">
<UpdateSafeModal onClose={toggleModal} safeAddress={safeAddress} />
</Modal>
</>
)}
</GnoForm>
</>
</Block>
<Row align="end" className={classes.controlsRow} grow>
<Col end="xs">
<Button
className={classes.saveBtn}
color="primary"
size="small"
testId={SAFE_NAME_SUBMIT_BTN_TEST_ID}
type="submit"
variant="contained"
>
Save
</Button>
</Col>
</Row>
<Modal description="Update Safe" handleClose={toggleModal} open={isModalOpen} title="Update Safe">
<UpdateSafeModal onClose={toggleModal} safeAddress={safeAddress} />
</Modal>
</>
)}
</GnoForm>
)
}