(Fix) remove module feature (#1646)

This commit is contained in:
Fernando 2020-11-25 15:35:09 -03:00 committed by GitHub
parent 53a1dcf0aa
commit dca9f19fd0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 163 additions and 22 deletions

View File

@ -6,7 +6,12 @@ export type SafeOwner = {
address: string address: string
} }
export type ModulePair = [string, string] export type ModulePair = [
// previous module
string,
// module
string,
]
export type SafeRecordProps = { export type SafeRecordProps = {
name: string name: string

View File

@ -0,0 +1,111 @@
import { SENTINEL_ADDRESS } from 'src/logic/contracts/safeContracts'
import { buildModulesLinkedList } from 'src/logic/safe/utils/modules'
describe('modules -> buildModulesLinkedList', () => {
let moduleManager
beforeEach(() => {
moduleManager = {
modules: {
[SENTINEL_ADDRESS]: SENTINEL_ADDRESS,
},
enableModule: function (module: string) {
this.modules[module] = this.modules[SENTINEL_ADDRESS]
this.modules[SENTINEL_ADDRESS] = module
},
disableModule: function (prevModule: string, module: string) {
this.modules[prevModule] = this.modules[module]
this.modules[module] = '0x0'
},
getModules: function (): string[] {
const modules: string[] = []
let module: string = this.modules[SENTINEL_ADDRESS]
while (module !== SENTINEL_ADDRESS) {
modules.push(module)
module = this.modules[module]
}
return modules
},
}
})
it(`should build a collection of addresses pair associated to a linked list`, () => {
// Given
const listOfModules = ['0xa', '0xb', '0xc', '0xd', '0xe', '0xf']
// When
const modulesPairList = buildModulesLinkedList(listOfModules)
// Then
expect(modulesPairList).toStrictEqual([
[SENTINEL_ADDRESS, '0xa'],
['0xa', '0xb'],
['0xb', '0xc'],
['0xc', '0xd'],
['0xd', '0xe'],
['0xe', '0xf'],
])
})
it(`should properly provide a list of modules pair to remove an specified module`, () => {
// Given
moduleManager.enableModule('0xc')
moduleManager.enableModule('0xb')
moduleManager.enableModule('0xa') // returned list is ordered [0xa, 0xb, 0xc]
const modulesPairList = buildModulesLinkedList(moduleManager.getModules())
// When
const moduleBPair = modulesPairList?.[1] ?? []
moduleManager.disableModule(...moduleBPair)
// Then
expect(moduleManager.modules['0xb']).toBe('0x0')
expect(moduleManager.getModules()).toStrictEqual(['0xa', '0xc'])
expect(buildModulesLinkedList(moduleManager.getModules())).toStrictEqual([
[SENTINEL_ADDRESS, '0xa'],
['0xa', '0xc'],
])
})
it(`should properly provide a list of modules pair to remove the firstly added module`, () => {
// Given
moduleManager.enableModule('0xc')
moduleManager.enableModule('0xb')
moduleManager.enableModule('0xa') // returned list is ordered [0xa, 0xb, 0xc]
const modulesPairList = buildModulesLinkedList(moduleManager.getModules())
// When
const moduleBPair = modulesPairList?.[2] ?? []
moduleManager.disableModule(...moduleBPair)
// Then
expect(moduleManager.modules['0xc']).toBe('0x0')
expect(moduleManager.getModules()).toStrictEqual(['0xa', '0xb'])
expect(buildModulesLinkedList(moduleManager.getModules())).toStrictEqual([
[SENTINEL_ADDRESS, '0xa'],
['0xa', '0xb'],
])
})
it(`should properly provide a list of modules pair to remove the lastly added module`, () => {
// Given
moduleManager.enableModule('0xc')
moduleManager.enableModule('0xb')
moduleManager.enableModule('0xa') // returned list is ordered [0xa, 0xb, 0xc]
const modulesPairList = buildModulesLinkedList(moduleManager.getModules())
// When
const moduleBPair = modulesPairList?.[0] ?? []
moduleManager.disableModule(...moduleBPair)
// Then
expect(moduleManager.modules['0xa']).toBe('0x0')
expect(moduleManager.getModules()).toStrictEqual(['0xb', '0xc'])
expect(buildModulesLinkedList(moduleManager.getModules())).toStrictEqual([
[SENTINEL_ADDRESS, '0xb'],
['0xb', '0xc'],
])
})
})

View File

@ -9,11 +9,32 @@ type ModulesPaginated = {
next: string next: string
} }
const buildModulesLinkedList = (modules: string[], nextModule: string = SENTINEL_ADDRESS): Array<ModulePair> | null => { /**
* Builds a collection of tuples with (prev, module) module addresses
*
* The `modules` param, is organized from the most recently added to the oldest.
*
* By assuming this, we are able to recreate the linked list that's defined at contract level
* considering `0x1` (SENTINEL_ADDRESS) address as the list's initial node.
*
* Given this scenario, we have a linked list in the form of
*
* **`0x1->modules[n]->module[n-1]->module[0]->0x1`**
*
* So,
* - if we want to disable `module[n]`, we need to pass `(module[n], 0x1)` as arguments,
* - if we want to disable `module[n-1]`, we need to pass `(module[n-1], module[n])`,
* - ... and so on
* @param {Array<string>} modules
* @returns null | Array<ModulePair>
*/
export const buildModulesLinkedList = (modules: string[]): Array<ModulePair> | null => {
if (modules?.length) { if (modules?.length) {
return modules.map((moduleAddress, index, modules) => { return modules.map((moduleAddress, index, modules) => {
const prevModule = modules[index + 1] if (index === 0) {
return [moduleAddress, prevModule !== undefined ? prevModule : nextModule] return [SENTINEL_ADDRESS, moduleAddress]
}
return [modules[index - 1], moduleAddress]
}) })
} }
@ -58,10 +79,12 @@ export const getModules = async (safeInfo: SafeInfo | void): Promise<Array<Modul
// as we're not sure if there are more than 10 modules enabled for the current Safe // as we're not sure if there are more than 10 modules enabled for the current Safe
const safeInstance = getGnosisSafeInstanceAt(safeInfo.address) const safeInstance = getGnosisSafeInstanceAt(safeInfo.address)
// TODO: 100 is an arbitrary large number, to avoid the need for pagination. But pagination must be properly handled // TODO: 100 is an arbitrary large number, to avoid the need for pagination.
// But pagination must be properly handled
// if `modules.next !== SENTINEL_ADDRESS`, then we have more modules to retrieve
const modules: ModulesPaginated = await safeInstance.methods.getModulesPaginated(SENTINEL_ADDRESS, 100).call() const modules: ModulesPaginated = await safeInstance.methods.getModulesPaginated(SENTINEL_ADDRESS, 100).call()
return buildModulesLinkedList(modules.array, modules.next) return buildModulesLinkedList(modules.array)
} catch (e) { } catch (e) {
console.error('Failed to retrieve Safe modules', e) console.error('Failed to retrieve Safe modules', e)
} }
@ -69,7 +92,7 @@ export const getModules = async (safeInfo: SafeInfo | void): Promise<Array<Modul
} }
export const getDisableModuleTxData = (modulePair: ModulePair, safeAddress: string): string => { export const getDisableModuleTxData = (modulePair: ModulePair, safeAddress: string): string => {
const [module, previousModule] = modulePair const [previousModule, module] = modulePair
const safeInstance = getGnosisSafeInstanceAt(safeAddress) const safeInstance = getGnosisSafeInstanceAt(safeAddress)
return safeInstance.methods.disableModule(previousModule, module).encodeABI() return safeInstance.methods.disableModule(previousModule, module).encodeABI()

View File

@ -50,9 +50,9 @@ const ModulesTable = ({ moduleData }: ModulesTableProps): React.ReactElement =>
const [viewRemoveModuleModal, setViewRemoveModuleModal] = React.useState(false) const [viewRemoveModuleModal, setViewRemoveModuleModal] = React.useState(false)
const hideRemoveModuleModal = () => setViewRemoveModuleModal(false) const hideRemoveModuleModal = () => setViewRemoveModuleModal(false)
const [selectedModule, setSelectedModule] = React.useState<ModulePair>() const [selectedModulePair, setSelectedModulePair] = React.useState<ModulePair>()
const triggerRemoveSelectedModule = (module: ModulePair): void => { const triggerRemoveSelectedModule = (modulePair: ModulePair): void => {
setSelectedModule(module) setSelectedModulePair(modulePair)
setViewRemoveModuleModal(true) setViewRemoveModuleModal(true)
} }
@ -80,14 +80,15 @@ const ModulesTable = ({ moduleData }: ModulesTableProps): React.ReactElement =>
{autoColumns.map((column, index) => { {autoColumns.map((column, index) => {
const columnId = column.id const columnId = column.id
const rowElement = row[columnId] const rowElement = row[columnId]
const [, moduleAddress] = rowElement
return ( return (
<React.Fragment key={`${columnId}-${index}`}> <React.Fragment key={`${columnId}-${index}`}>
<TableCell align={column.align} component="td" key={columnId}> <TableCell align={column.align} component="td" key={columnId}>
{columnId === MODULES_TABLE_ADDRESS_ID ? ( {columnId === MODULES_TABLE_ADDRESS_ID ? (
<Block justify="left"> <Block justify="left">
<Identicon address={rowElement[0]} diameter={32} /> <Identicon address={moduleAddress} diameter={32} />
<AddressText size="lg">{rowElement[0]}</AddressText> <AddressText size="lg">{moduleAddress}</AddressText>
</Block> </Block>
) : ( ) : (
rowElement rowElement
@ -117,8 +118,8 @@ const ModulesTable = ({ moduleData }: ModulesTableProps): React.ReactElement =>
} }
</Table> </Table>
</TableContainer> </TableContainer>
{viewRemoveModuleModal && selectedModule && ( {viewRemoveModuleModal && selectedModulePair && (
<RemoveModuleModal onClose={hideRemoveModuleModal} selectedModule={selectedModule} /> <RemoveModuleModal onClose={hideRemoveModuleModal} selectedModulePair={selectedModulePair} />
)} )}
</> </>
) )

View File

@ -41,21 +41,22 @@ const openIconStyle = {
interface RemoveModuleModalProps { interface RemoveModuleModalProps {
onClose: () => void onClose: () => void
selectedModule: ModulePair selectedModulePair: ModulePair
} }
const RemoveModuleModal = ({ onClose, selectedModule }: RemoveModuleModalProps): React.ReactElement => { const RemoveModuleModal = ({ onClose, selectedModulePair }: RemoveModuleModalProps): React.ReactElement => {
const classes = useStyles() const classes = useStyles()
const safeAddress = useSelector(safeParamAddressFromStateSelector) const safeAddress = useSelector(safeParamAddressFromStateSelector)
const dispatch = useDispatch() const dispatch = useDispatch()
const explorerInfo = getExplorerInfo(selectedModule[0]) const [, moduleAddress] = selectedModulePair
const explorerInfo = getExplorerInfo(moduleAddress)
const { url } = explorerInfo() const { url } = explorerInfo()
const removeSelectedModule = async (): Promise<void> => { const removeSelectedModule = async (): Promise<void> => {
try { try {
const txData = getDisableModuleTxData(selectedModule, safeAddress) const txData = getDisableModuleTxData(selectedModulePair, safeAddress)
dispatch( dispatch(
createTransaction({ createTransaction({
@ -67,7 +68,7 @@ const RemoveModuleModal = ({ onClose, selectedModule }: RemoveModuleModalProps):
}), }),
) )
} catch (e) { } catch (e) {
console.error(`failed to remove the module ${selectedModule}`, e.message) console.error(`failed to remove the module ${selectedModulePair}`, e.message)
} }
} }
@ -92,16 +93,16 @@ const RemoveModuleModal = ({ onClose, selectedModule }: RemoveModuleModalProps):
<Block className={classes.modalContainer}> <Block className={classes.modalContainer}>
<Row className={classes.modalOwner}> <Row className={classes.modalOwner}>
<Col align="center" xs={1}> <Col align="center" xs={1}>
<Identicon address={selectedModule[0]} diameter={32} /> <Identicon address={moduleAddress} diameter={32} />
</Col> </Col>
<Col xs={11}> <Col xs={11}>
<Block className={cn(classes.modalName, classes.modalUserName)}> <Block className={cn(classes.modalName, classes.modalUserName)}>
<Paragraph noMargin size="lg" weight="bolder"> <Paragraph noMargin size="lg" weight="bolder">
{selectedModule[0]} {moduleAddress}
</Paragraph> </Paragraph>
<Block className={classes.modalUser} justify="center"> <Block className={classes.modalUser} justify="center">
<Paragraph color="disabled" noMargin size="md"> <Paragraph color="disabled" noMargin size="md">
{selectedModule[0]} {moduleAddress}
</Paragraph> </Paragraph>
<Link className={classes.modalOpen} target="_blank" to={url}> <Link className={classes.modalOpen} target="_blank" to={url}>
<OpenInNew style={openIconStyle} /> <OpenInNew style={openIconStyle} />