(Fix) Modules not shown in Advanced Settings (#1516)

This commit is contained in:
Fernando 2020-10-27 11:04:44 -03:00 committed by GitHub
parent 4ce8917f34
commit aa181fb9a1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 189 additions and 108 deletions

View File

@ -1,8 +1,9 @@
import memoize from 'lodash.memoize'
import networks from 'src/config/networks'
import { EnvironmentSettings, ETHEREUM_NETWORK, NetworkSettings, SafeFeatures, Wallets, GasPriceOracle } from 'src/config/networks/network.d'
import { APP_ENV, ETHERSCAN_API_KEY, GOOGLE_ANALYTICS_ID, INFURA_TOKEN, NETWORK, NODE_ENV } from 'src/utils/constants'
import { ensureOnce } from 'src/utils/singleton'
import memoize from 'lodash.memoize'
export const getNetworkId = (): ETHEREUM_NETWORK => ETHEREUM_NETWORK[NETWORK]
@ -57,11 +58,11 @@ const configuration = (): NetworkSpecificConfiguration => {
const getConfig: () => NetworkSpecificConfiguration = ensureOnce(configuration)
export const getTxServiceUrl = (): string => getConfig()?.txServiceUrl
export const getTxServiceUrl = (): string => getConfig().txServiceUrl
export const getRelayUrl = (): string | undefined => getConfig()?.relayApiUrl
export const getRelayUrl = (): string | undefined => getConfig().relayApiUrl
export const getGnosisSafeAppsUrl = (): string => getConfig()?.safeAppsUrl
export const getGnosisSafeAppsUrl = (): string => getConfig().safeAppsUrl
export const getGasPrice = (): number | undefined => getConfig()?.gasPrice
@ -71,31 +72,27 @@ export const getRpcServiceUrl = (): string => {
const usesInfuraRPC = [ETHEREUM_NETWORK.MAINNET, ETHEREUM_NETWORK.RINKEBY].includes(getNetworkId())
if (usesInfuraRPC) {
return `${getConfig()?.rpcServiceUrl}/${INFURA_TOKEN}`
return `${getConfig().rpcServiceUrl}/${INFURA_TOKEN}`
}
return getConfig()?.rpcServiceUrl
return getConfig().rpcServiceUrl
}
export const getSafeServiceBaseUrl = (safeAddress: string) => `${getTxServiceUrl()}/safes/${safeAddress}`
export const getTokensServiceBaseUrl = () => `${getTxServiceUrl()}/tokens`
export const getNetworkExplorerInfo = (): { name: string; url: string; apiUrl: string } => ({
name: getConfig()?.networkExplorerName,
url: getConfig()?.networkExplorerUrl,
apiUrl: getConfig()?.networkExplorerApiUrl,
name: getConfig().networkExplorerName,
url: getConfig().networkExplorerUrl,
apiUrl: getConfig().networkExplorerApiUrl,
})
export const getNetworkConfigDisabledFeatures = (): SafeFeatures => getConfig()?.disabledFeatures || []
export const getNetworkConfigDisabledFeatures = (): SafeFeatures => getConfig().disabledFeatures || []
export const getNetworkConfigDisabledWallets = (): Wallets => getConfig()?.disabledWallets || []
export const getNetworkInfo = (): NetworkSettings => getConfig()?.network
export const getTxServiceUriFrom = (safeAddress: string) => `/safes/${safeAddress}/transactions/`
export const getIncomingTxServiceUriTo = (safeAddress: string) => `/safes/${safeAddress}/incoming-transfers/`
export const getAllTransactionsUriFrom = (safeAddress: string) => `/safes/${safeAddress}/all-transactions/`
export const getSafeCreationTxUri = (safeAddress: string) => `/safes/${safeAddress}/creation/`
export const getNetworkInfo = (): NetworkSettings => getConfig().network
export const getGoogleAnalyticsTrackingID = (): string => GOOGLE_ANALYTICS_ID
@ -163,11 +160,11 @@ export const getExplorerInfo = (hash: string): BlockScanInfo => {
switch (networkInfo.id) {
default: {
const type = hash.length > 42 ? 'tx' : 'address'
const type = hash.length > 42 ? 'tx' : 'address'
return () => ({
url: `${url}/${type}/${hash}`,
alt: name || '',
})
}
}
}
}

View File

@ -1,7 +1,8 @@
import { aNewStore } from 'src/store'
import { fetchTokenCurrenciesBalances } from 'src/logic/currencyValues/api/fetchTokenCurrenciesBalances'
import axios from 'axios'
import { getTxServiceUrl } from 'src/config'
import { getSafeServiceBaseUrl } from 'src/config'
import { fetchTokenCurrenciesBalances } from 'src/logic/currencyValues/api/fetchTokenCurrenciesBalances'
import { aNewStore } from 'src/store'
jest.mock('axios')
describe('fetchTokenCurrenciesBalances', () => {
@ -40,7 +41,7 @@ describe('fetchTokenCurrenciesBalances', () => {
fiatCode: 'USD',
},
]
const apiUrl = getTxServiceUrl()
const apiUrl = getSafeServiceBaseUrl(safeAddress)
// @ts-ignore
axios.get.mockImplementationOnce(() => Promise.resolve(expectedResult))
@ -51,6 +52,6 @@ describe('fetchTokenCurrenciesBalances', () => {
// then
expect(result).toStrictEqual(expectedResult)
expect(axios.get).toHaveBeenCalled()
expect(axios.get).toBeCalledWith(`${apiUrl}/safes/${safeAddress}/balances/usd/?exclude_spam=${excludeSpamTokens}`)
expect(axios.get).toBeCalledWith(`${apiUrl}/balances/usd/?exclude_spam=${excludeSpamTokens}`)
})
})

View File

@ -1,6 +1,6 @@
import axios, { AxiosResponse } from 'axios'
import { getTxServiceUrl } from 'src/config'
import { getSafeServiceBaseUrl } from 'src/config'
import { TokenProps } from 'src/logic/tokens/store/model/token'
export type BalanceEndpoint = {
@ -16,8 +16,7 @@ export const fetchTokenCurrenciesBalances = (
safeAddress: string,
excludeSpamTokens = true,
): Promise<AxiosResponse<BalanceEndpoint[]>> => {
const apiUrl = getTxServiceUrl()
const url = `${apiUrl}/safes/${safeAddress}/balances/usd/?exclude_spam=${excludeSpamTokens}`
const url = `${getSafeServiceBaseUrl(safeAddress)}/balances/usd/?exclude_spam=${excludeSpamTokens}`
return axios.get(url)
}

View File

@ -1,8 +1,8 @@
import axios, { AxiosResponse } from 'axios'
import { getAllTransactionsUriFrom, getTxServiceUrl } from 'src/config'
import { getSafeServiceBaseUrl } from 'src/config'
import { checksumAddress } from 'src/utils/checksumAddress'
import { Transaction } from '../../models/types/transactions.d'
import { Transaction } from 'src/logic/safe/store/models/types/transactions.d'
export type ServiceUriParams = {
safeAddress: string
@ -21,11 +21,8 @@ type TransactionDTO = {
}
const getAllTransactionsUri = (safeAddress: string): string => {
const host = getTxServiceUrl()
const address = checksumAddress(safeAddress)
const base = getAllTransactionsUriFrom(address)
return `${host}/${base}`
return `${getSafeServiceBaseUrl(address)}/all-transactions/`
}
const fetchAllTransactions = async (

View File

@ -13,10 +13,11 @@ import removeSafeOwner from 'src/logic/safe/store/actions/removeSafeOwner'
import updateSafe from 'src/logic/safe/store/actions/updateSafe'
import { makeOwner } from 'src/logic/safe/store/models/owner'
import { checksumAddress } from 'src/utils/checksumAddress'
import { ModulePair, SafeOwner, SafeRecordProps } from 'src/logic/safe/store/models/safe'
import { SENTINEL_ADDRESS } from 'src/logic/contracts/safeContracts'
import { SafeOwner, SafeRecordProps } from 'src/logic/safe/store/models/safe'
import { AppReduxState } from 'src/store'
import { latestMasterContractVersionSelector } from '../selectors'
import { latestMasterContractVersionSelector } from 'src/logic/safe/store/selectors'
import { getSafeInfo } from 'src/logic/safe/utils/safeInformation'
import { getModules } from 'src/logic/safe/utils/modules'
const buildOwnersFrom = (safeOwners: string[], localSafe?: SafeRecordProps): List<SafeOwner> => {
const ownersList = safeOwners.map((ownerAddress) => {
@ -40,16 +41,6 @@ const buildOwnersFrom = (safeOwners: string[], localSafe?: SafeRecordProps): Lis
return List(ownersList)
}
const buildModulesLinkedList = (modules?: string[], nextModule?: string): Array<ModulePair> | null => {
if (modules?.length && nextModule) {
return modules.map((moduleAddress, index, modules) => {
const prevModule = modules[index + 1]
return [moduleAddress, prevModule !== undefined ? prevModule : nextModule]
})
}
return null
}
export const buildSafe = async (
safeAdd: string,
safeName: string,
@ -58,12 +49,18 @@ export const buildSafe = async (
const safeAddress = checksumAddress(safeAdd)
const safeParams = ['getThreshold', 'nonce', 'VERSION', 'getOwners']
const [[, thresholdStr, nonceStr, currentVersion, remoteOwners = []], localSafe, ethBalance] = await Promise.all([
const [
[, thresholdStr, nonceStr, currentVersion, remoteOwners = []],
safeInfo,
localSafe,
ethBalance,
] = await Promise.all([
generateBatchRequests<[undefined, string | undefined, string | undefined, string | undefined, string[]]>({
abi: GnosisSafeSol.abi as AbiItem[],
address: safeAddress,
methods: safeParams,
}),
getSafeInfo(safeAddress),
getLocalSafe(safeAddress),
getBalanceInEtherOf(safeAddress),
])
@ -73,6 +70,7 @@ export const buildSafe = async (
const owners = buildOwnersFrom(remoteOwners, localSafe)
const needsUpdate = safeNeedsUpdate(currentVersion, latestMasterContractVersion)
const featuresEnabled = enabledFeatures(currentVersion)
const modules = await getModules(safeInfo)
return {
address: safeAddress,
@ -90,51 +88,34 @@ export const buildSafe = async (
activeTokens: Set(),
blacklistedAssets: Set(),
blacklistedTokens: Set(),
modules: null,
modules,
}
}
export const checkAndUpdateSafe = (safeAdd: string) => async (dispatch: Dispatch): Promise<void> => {
const safeAddress = checksumAddress(safeAdd)
// Check if the owner's safe did change and update them
const safeParams = [
'getThreshold',
'nonce',
'getOwners',
// TODO: 100 is an arbitrary large number, to avoid the need for pagination. But pagination must be properly handled
{ method: 'getModulesPaginated', args: [SENTINEL_ADDRESS, 100] },
]
const [[, remoteThreshold, remoteNonce, remoteOwners, modules], localSafe] = await Promise.all([
generateBatchRequests<
[
undefined,
string | undefined,
string | undefined,
string[],
(
| {
array: string[]
next: string
}
| undefined
),
]
>({
const safeParams = ['getThreshold', 'nonce', 'getOwners']
const [[, remoteThreshold, remoteNonce, remoteOwners = []], safeInfo, localSafe] = await Promise.all([
generateBatchRequests<[undefined, string | undefined, string | undefined, string[]]>({
abi: GnosisSafeSol.abi as AbiItem[],
address: safeAddress,
methods: safeParams,
}),
getSafeInfo(safeAddress),
getLocalSafe(safeAddress),
])
// Converts from [ { address, ownerName} ] to address array
const localOwners = localSafe ? localSafe.owners.map((localOwner) => localOwner.address) : []
const modules = await getModules(safeInfo)
dispatch(
updateSafe({
address: safeAddress,
name: localSafe?.name,
modules: buildModulesLinkedList(modules?.array, modules?.next),
modules,
nonce: Number(remoteNonce),
threshold: Number(remoteThreshold),
featuresEnabled: localSafe?.currentVersion

View File

@ -14,7 +14,7 @@ export type SafeRecordProps = {
threshold: number
ethBalance: string
owners: List<SafeOwner>
modules: ModulePair[] | null
modules?: ModulePair[] | null
activeTokens: Set<string>
activeAssets: Set<string>
blacklistedTokens: Set<string>

View File

@ -1,10 +1,7 @@
import { getIncomingTxServiceUriTo, getTxServiceUrl } from 'src/config'
import { getSafeServiceBaseUrl } from 'src/config'
import { checksumAddress } from 'src/utils/checksumAddress'
export const buildIncomingTxServiceUrl = (safeAddress: string): string => {
const host = getTxServiceUrl()
const address = checksumAddress(safeAddress)
const base = getIncomingTxServiceUriTo(address)
return `${host}/${base}`
return `${getSafeServiceBaseUrl(address)}/incoming-transfers/`
}

View File

@ -1,7 +1,7 @@
import axios from 'axios'
import { GnosisSafe } from 'src/types/contracts/GnosisSafe.d'
import { getTxServiceUrl, getTxServiceUriFrom } from 'src/config'
import { getSafeServiceBaseUrl } from 'src/config'
import { checksumAddress } from 'src/utils/checksumAddress'
const calculateBodyFrom = async (
@ -45,10 +45,8 @@ const calculateBodyFrom = async (
}
export const buildTxServiceUrl = (safeAddress: string): string => {
const host = getTxServiceUrl()
const address = checksumAddress(safeAddress)
const base = getTxServiceUriFrom(address)
return `${host}/${base}?has_confirmations=True`
return `${getSafeServiceBaseUrl(address)}/transactions/?has_confirmations=True`
}
const SUCCESS_STATUS = 201 // CREATED status

View File

@ -1,10 +1,7 @@
import { getTxServiceUrl, getSafeCreationTxUri } from 'src/config'
import { getSafeServiceBaseUrl } from 'src/config'
import { checksumAddress } from 'src/utils/checksumAddress'
export const buildSafeCreationTxUrl = (safeAddress: string): string => {
const host = getTxServiceUrl()
const address = checksumAddress(safeAddress)
const base = getSafeCreationTxUri(address)
return `${host}/${base}`
return `${getSafeServiceBaseUrl(address)}/creation/`
}

View File

@ -0,0 +1,8 @@
import { getSafeServiceBaseUrl } from 'src/config'
import { checksumAddress } from 'src/utils/checksumAddress'
export const buildSafeInformationUrl = (safeAddress: string): string => {
const address = checksumAddress(safeAddress)
const url = getSafeServiceBaseUrl(address)
return `${url}/`
}

View File

@ -0,0 +1,76 @@
import semverLessThan from 'semver/functions/lt'
import { getGnosisSafeInstanceAt, SENTINEL_ADDRESS } from 'src/logic/contracts/safeContracts'
import { ModulePair } from 'src/logic/safe/store/models/safe'
import { SafeInfo } from 'src/logic/safe/utils/safeInformation'
type ModulesPaginated = {
array: string[]
next: string
}
const buildModulesLinkedList = (modules: string[], nextModule: string = SENTINEL_ADDRESS): Array<ModulePair> | null => {
if (modules?.length) {
return modules.map((moduleAddress, index, modules) => {
const prevModule = modules[index + 1]
return [moduleAddress, prevModule !== undefined ? prevModule : nextModule]
})
}
// no modules
return null
}
/**
* Returns a list of Modules if there's any, in the form of [module, prevModule][]
* so we have an easy track of the prevModule and the currentModule when calling `disableModule`
*
* There's a slight difference on how many modules `getModules` return, depending on the Safe's version we're in:
* - for >= v1.1.1 it will return a list of up to 10 modules
* - for previous version it will return a list of all the modules enabled
*
* As we're using the safe-transactions service, and it's querying `getModules`,
* we'll fallback to `getModulesPaginated` RPC call when needed.
*
* @todo: Implement pagination for `getModulesPaginated`. We're passing an arbitrary large number to avoid pagination.
*
* @param {SafeInfo | undefined } safeInfo
* @returns Array<ModulePair> | null | undefined
*/
export const getModules = async (safeInfo: SafeInfo | void): Promise<Array<ModulePair> | null | undefined> => {
if (!safeInfo) {
return
}
if (semverLessThan(safeInfo.version, '1.1.1')) {
// we can use the `safeInfo.modules`, as versions previous to 1.1.1 return the whole list of modules
return buildModulesLinkedList(safeInfo.modules)
} else {
// newer versions `getModules` call returns up to 10 modules
if (safeInfo.modules.length < 10) {
// we're sure that we got all the modules
return buildModulesLinkedList(safeInfo.modules)
}
try {
// lastly, if `safeInfo.modules` have 10 items,
// we'll fallback to `getModulesPaginated` RPC call
// as we're not sure if there are more than 10 modules enabled for the current Safe
const safeInstance = getGnosisSafeInstanceAt(safeInfo.address)
// TODO: 100 is an arbitrary large number, to avoid the need for pagination. But pagination must be properly handled
const modules: ModulesPaginated = await safeInstance.methods.getModulesPaginated(SENTINEL_ADDRESS, 100).call()
return buildModulesLinkedList(modules.array, modules.next)
} catch (e) {
console.error('Failed to retrieve Safe modules', e)
}
}
}
export const getDisableModuleTxData = (modulePair: ModulePair, safeAddress: string): string => {
const [module, previousModule] = modulePair
const safeInstance = getGnosisSafeInstanceAt(safeAddress)
return safeInstance.methods.disableModule(previousModule, module).encodeABI()
}

View File

@ -0,0 +1,32 @@
import axios, { AxiosError, AxiosResponse } from 'axios'
import { buildSafeInformationUrl } from './buildSafeInformationUrl'
export type SafeInfo = {
address: string
nonce: number
threshold: number
owners: string[]
masterCopy: string
modules: string[]
fallbackHandler: string
version: string
}
export type SafeInfoError = {
code: number
message: string
arguments: string[]
}
export const getSafeInfo = (safeAddress: string): Promise<void | SafeInfo> => {
const safeInfoUrl = buildSafeInformationUrl(safeAddress)
return axios
.get<SafeInfo, AxiosResponse<SafeInfo>>(safeInfoUrl)
.then((response) => response.data)
.catch((error: AxiosError<SafeInfoError>) => {
console.error(
'Failed to retrieve safe Information',
error.response?.statusText ?? error.response?.data.message ?? error,
)
})
}

View File

@ -1,6 +1,6 @@
import axios, { AxiosResponse } from 'axios'
import { getTxServiceUrl } from 'src/config'
import { getTokensServiceBaseUrl } from 'src/config'
export type TokenResult = {
address: string
@ -12,11 +12,9 @@ export type TokenResult = {
}
export const fetchErc20AndErc721AssetsList = async (): Promise<AxiosResponse<{ results: TokenResult[] }>> => {
const apiUrl = getTxServiceUrl()
const url = getTokensServiceBaseUrl()
const url = `${apiUrl}/tokens/`
return axios.get<{ results: TokenResult[] }>(url, {
return axios.get<{ results: TokenResult[] }>(`${url}/`, {
params: {
limit: 3000,
},

View File

@ -1,6 +1,7 @@
import axios, { AxiosResponse } from 'axios'
import { getTxServiceUrl } from 'src/config'
import { getSafeServiceBaseUrl } from 'src/config'
import { checksumAddress } from 'src/utils/checksumAddress'
export type CollectibleResult = {
address: string
@ -16,9 +17,8 @@ export type CollectibleResult = {
}
export const fetchSafeCollectibles = async (safeAddress: string): Promise<AxiosResponse<CollectibleResult[]>> => {
const apiUrl = getTxServiceUrl()
const url = `${apiUrl}/safes/${safeAddress}/collectibles/`
const address = checksumAddress(safeAddress)
const url = `${getSafeServiceBaseUrl(address)}/collectibles/`
return axios.get(url)
}

View File

@ -1,7 +1,8 @@
import axios, { AxiosResponse } from 'axios'
import { getTxServiceUrl } from 'src/config'
import { getSafeServiceBaseUrl } from 'src/config'
import { TokenProps } from 'src/logic/tokens/store/model/token'
import { checksumAddress } from 'src/utils/checksumAddress'
type BalanceResult = {
tokenAddress: string
@ -10,8 +11,8 @@ type BalanceResult = {
}
export const fetchTokenBalanceList = (safeAddress: string): Promise<AxiosResponse<{ results: BalanceResult[] }>> => {
const apiUrl = getTxServiceUrl()
const url = `${apiUrl}/safes/${safeAddress}/balances/`
const address = checksumAddress(safeAddress)
const url = `${getSafeServiceBaseUrl(address)}/balances/`
return axios.get(url)
}

View File

@ -6,6 +6,8 @@ import OpenInNew from '@material-ui/icons/OpenInNew'
import cn from 'classnames'
import React from 'react'
import { useDispatch, useSelector } from 'react-redux'
import styled from 'styled-components'
import Identicon from 'src/components/Identicon'
import Block from 'src/components/layout/Block'
import Col from 'src/components/layout/Col'
@ -15,14 +17,13 @@ import Paragraph from 'src/components/layout/Paragraph'
import Row from 'src/components/layout/Row'
import Modal from 'src/components/Modal'
import { getExplorerInfo } from 'src/config'
import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
import { getDisableModuleTxData } from 'src/logic/safe/utils/modules'
import createTransaction from 'src/logic/safe/store/actions/createTransaction'
import { ModulePair } from 'src/logic/safe/store/models/safe'
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
import { md, secondary } from 'src/theme/variables'
import styled from 'styled-components'
import { styles } from './style'
@ -46,7 +47,7 @@ interface RemoveModuleModal {
const RemoveModuleModal = ({ onClose, selectedModule }: RemoveModuleModal): React.ReactElement => {
const classes = useStyles()
const safeAddress = useSelector(safeParamAddressFromStateSelector) as string
const safeAddress = useSelector(safeParamAddressFromStateSelector)
const dispatch = useDispatch()
const explorerInfo = getExplorerInfo(selectedModule[0])
@ -54,9 +55,7 @@ const RemoveModuleModal = ({ onClose, selectedModule }: RemoveModuleModal): Reac
const removeSelectedModule = async (): Promise<void> => {
try {
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
const [module, prevModule] = selectedModule
const txData = safeInstance.methods.disableModule(prevModule, module).encodeABI()
const txData = getDisableModuleTxData(selectedModule, safeAddress)
dispatch(
createTransaction({

View File

@ -83,7 +83,7 @@ const Advanced = (): React.ReactElement => {
.
</InfoText>
{moduleData === null ? (
{!moduleData ? (
<NoModuleLegend />
) : moduleData?.length === 0 ? (
<LoadingModules />