mirror of
https://github.com/status-im/safe-react.git
synced 2026-08-30 21:21:10 +00:00
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:
co-authored by
Daniel Sanchez
parent
e3a8b9fe2a
commit
0ecd70fdbc
+3
-3
@@ -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
|
||||
@@ -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
@@ -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()
|
||||
|
||||
@@ -60,7 +60,7 @@ const mainnet: NetworkConfig = {
|
||||
WALLETS.WALLET_LINK,
|
||||
WALLETS.AUTHEREUM,
|
||||
WALLETS.LATTICE,
|
||||
]
|
||||
],
|
||||
}
|
||||
|
||||
export default mainnet
|
||||
|
||||
@@ -11,5 +11,5 @@ export default {
|
||||
rinkeby,
|
||||
xdai,
|
||||
energy_web_chain,
|
||||
volta
|
||||
volta,
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ const mainnet: NetworkConfig = {
|
||||
decimals: 18,
|
||||
logoUri: EtherLogo,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default mainnet
|
||||
|
||||
Vendored
+16
-14
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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,4 +1,4 @@
|
||||
//
|
||||
//
|
||||
|
||||
// Code of the safe v1.0.0
|
||||
const proxyCodeV10 =
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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])
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user