Skip to content

Commit

Permalink
add: data management tab with import all data button (#1103)
Browse files Browse the repository at this point in the history
* add: data management tab with import all data button
* add: /import route which contains a global data import page
* ignore rinkeby for addressbook migration

Co-authored-by: Aaron Cook <[email protected]>
Co-authored-by: Usame Algan <[email protected]>
  • Loading branch information
3 people committed Nov 11, 2022
1 parent bd3a2e1 commit a26e5a7
Show file tree
Hide file tree
Showing 17 changed files with 623 additions and 1 deletion.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
"qrcode.react": "^3.1.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-dropzone": "^14.2.3",
"react-gtm-module": "^2.0.11",
"react-hook-form": "^7.36.1",
"react-papaparse": "^4.0.2",
Expand Down
7 changes: 7 additions & 0 deletions public/images/settings/data/file.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
48 changes: 48 additions & 0 deletions src/components/settings/DataManagement/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import InfoIcon from '@/public/images/notifications/info.svg'

import Track from '@/components/common/Track'
import { SETTINGS_EVENTS } from '@/services/analytics'
import { Paper, Grid, Typography, Tooltip, SvgIcon, Button } from '@mui/material'
import { useState } from 'react'
import ImportAllDialog from '../ImportAllDialog'

const DataManagement = () => {
const [modalOpen, setModalOpen] = useState(false)

return (
<Paper sx={{ p: 4, mb: 2 }}>
<Grid container spacing={3}>
<Grid item sm={4} xs={12}>
<Typography variant="h4" fontWeight={700}>
Data import
<Tooltip
placement="top"
title="The imported data will overwrite all added Safes and all address book entries"
>
<span>
<SvgIcon
component={InfoIcon}
inheritViewBox
fontSize="small"
color="border"
sx={{ verticalAlign: 'middle', ml: 0.5 }}
/>
</span>
</Tooltip>
</Typography>
</Grid>
<Grid item xs justifyContent="flex-end" display="flex">
<Track {...SETTINGS_EVENTS.DATA.IMPORT_ALL_BUTTON}>
<Button size="small" variant="contained" onClick={() => setModalOpen(true)}>
Import all data
</Button>
</Track>

{modalOpen && <ImportAllDialog handleClose={() => setModalOpen(false)} />}
</Grid>
</Grid>
</Paper>
)
}

export default DataManagement
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { renderHook } from '@/tests/test-utils'
import { ImportErrors, useGlobalImportJsonParser } from '../useGlobalImportFileParser'

describe('useGlobalImportFileParser', () => {
it('should return undefined values for undefined json', () => {
const { result } = renderHook(() => useGlobalImportJsonParser(undefined))
expect(result.current).toEqual({
addedSafes: undefined,
addressBook: undefined,
addressBookEntriesCount: 0,
addedSafesCount: 0,
})
})

it('should return undefined values and error for empty json', () => {
const { result } = renderHook(() => useGlobalImportJsonParser('{ "version": "1.0", "data": "{}" }'))
expect(result.current).toEqual({
addedSafes: undefined,
addressBook: undefined,
addressBookEntriesCount: 0,
addedSafesCount: 0,
error: ImportErrors.NO_IMPORT_DATA_FOUND,
})
})

it('should return empty objects for invalid json', () => {
const { result } = renderHook(() => useGlobalImportJsonParser('{ invalid: json, '))
expect(result.current).toEqual({
addedSafes: undefined,
addressBook: undefined,
addressBookEntriesCount: 0,
addedSafesCount: 0,
error: ImportErrors.INVALID_JSON_FORMAT,
})
})

it('should return empty objects for wrong versions', () => {
const goerliSafeAddress = '0xAecDFD3A19f777F0c03e6bf99AAfB59937d6467b'
const mainnetSafeAddress = '0x7cB6E6Cbc845e79d9CA05F6577141DA36ad398f5'

const owner1 = '0x3819b800c67Be64029C1393c8b2e0d0d627dADE2'
const owner2 = '0x954cD69f0E902439f99156e3eeDA080752c08401'

const jsonData = JSON.stringify({
version: '2.0',
data: {
'_immortal|v2_5__SAFES': `{"${goerliSafeAddress}":{"address":"${goerliSafeAddress}","chainId":"5","threshold":2,"ethBalance":"0.3","totalFiatBalance":"435.08","owners":["${owner1}","${owner2}"],"modules":[],"spendingLimits":[],"balances":[{"tokenAddress":"0x0000000000000000000000000000000000000000","fiatBalance":"435.08100","tokenBalance":"0.3"},{"tokenAddress":"0x61fD3b6d656F39395e32f46E2050953376c3f5Ff","fiatBalance":"0.00000","tokenBalance":"22405.086233211233211233"}],"implementation":{"value":"0x3E5c63644E683549055b9Be8653de26E0B4CD36E"},"loaded":true,"nonce":1,"currentVersion":"1.3.0+L2","needsUpdate":false,"featuresEnabled":["CONTRACT_INTERACTION","DOMAIN_LOOKUP","EIP1559","ERC721","SAFE_APPS","SAFE_TX_GAS_OPTIONAL","SPENDING_LIMIT","TX_SIMULATION","WARNING_BANNER"],"loadedViaUrl":false,"guard":"","collectiblesTag":"1667921524","txQueuedTag":"1667921524","txHistoryTag":"1667400927"}}`,
'_immortal|v2_MAINNET__SAFES': `{"${mainnetSafeAddress}":{"address":"${mainnetSafeAddress}","chainId":"1","threshold":1,"ethBalance":"0","totalFiatBalance":"0.00","owners":["${owner1}","${owner2}"],"modules":[],"spendingLimits":[],"balances":[{"tokenAddress":"0x0000000000000000000000000000000000000000","fiatBalance":"0.00000","tokenBalance":"0"}],"implementation":{"value":"0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552","name":"Gnosis Safe: Singleton 1.3.0","logoUri":"https://safe-transaction-assets.safe.global/contracts/logos/0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552.png"},"loaded":true,"nonce":2,"currentVersion":"1.3.0","needsUpdate":false,"featuresEnabled":["CONTRACT_INTERACTION","DOMAIN_LOOKUP","EIP1559","ERC721","SAFE_APPS","SAFE_TX_GAS_OPTIONAL","SPENDING_LIMIT","TX_SIMULATION"],"loadedViaUrl":false,"guard":"","collectiblesTag":"1667397095","txQueuedTag":"1667397095","txHistoryTag":"1664287235"}}`,
},
})

const { result } = renderHook(() => useGlobalImportJsonParser(jsonData))

expect(result.current).toEqual({
addedSafes: undefined,
addressBook: undefined,
addressBookEntriesCount: 0,
addedSafesCount: 0,
error: ImportErrors.INVALID_VERSION,
})
})

it('should parse added safes correctly', () => {
const goerliSafeAddress = '0xAecDFD3A19f777F0c03e6bf99AAfB59937d6467b'
const mainnetSafeAddress = '0x7cB6E6Cbc845e79d9CA05F6577141DA36ad398f5'

const owner1 = '0x3819b800c67Be64029C1393c8b2e0d0d627dADE2'
const owner2 = '0x954cD69f0E902439f99156e3eeDA080752c08401'

const jsonData = JSON.stringify({
version: '1.0',
data: {
'_immortal|v2_5__SAFES': `{"${goerliSafeAddress}":{"address":"${goerliSafeAddress}","chainId":"5","threshold":2,"ethBalance":"0.3","totalFiatBalance":"435.08","owners":["${owner1}","${owner2}"],"modules":[],"spendingLimits":[],"balances":[{"tokenAddress":"0x0000000000000000000000000000000000000000","fiatBalance":"435.08100","tokenBalance":"0.3"},{"tokenAddress":"0x61fD3b6d656F39395e32f46E2050953376c3f5Ff","fiatBalance":"0.00000","tokenBalance":"22405.086233211233211233"}],"implementation":{"value":"0x3E5c63644E683549055b9Be8653de26E0B4CD36E"},"loaded":true,"nonce":1,"currentVersion":"1.3.0+L2","needsUpdate":false,"featuresEnabled":["CONTRACT_INTERACTION","DOMAIN_LOOKUP","EIP1559","ERC721","SAFE_APPS","SAFE_TX_GAS_OPTIONAL","SPENDING_LIMIT","TX_SIMULATION","WARNING_BANNER"],"loadedViaUrl":false,"guard":"","collectiblesTag":"1667921524","txQueuedTag":"1667921524","txHistoryTag":"1667400927"}}`,
'_immortal|v2_MAINNET__SAFES': `{"${mainnetSafeAddress}":{"address":"${mainnetSafeAddress}","chainId":"1","threshold":1,"ethBalance":"0","totalFiatBalance":"0.00","owners":["${owner1}","${owner2}"],"modules":[],"spendingLimits":[],"balances":[{"tokenAddress":"0x0000000000000000000000000000000000000000","fiatBalance":"0.00000","tokenBalance":"0"}],"implementation":{"value":"0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552","name":"Gnosis Safe: Singleton 1.3.0","logoUri":"https://safe-transaction-assets.safe.global/contracts/logos/0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552.png"},"loaded":true,"nonce":2,"currentVersion":"1.3.0","needsUpdate":false,"featuresEnabled":["CONTRACT_INTERACTION","DOMAIN_LOOKUP","EIP1559","ERC721","SAFE_APPS","SAFE_TX_GAS_OPTIONAL","SPENDING_LIMIT","TX_SIMULATION"],"loadedViaUrl":false,"guard":"","collectiblesTag":"1667397095","txQueuedTag":"1667397095","txHistoryTag":"1664287235"}}`,
},
})
const { result } = renderHook(() => useGlobalImportJsonParser(jsonData))

const { addedSafes, addedSafesCount, addressBook, addressBookEntriesCount } = result.current

// No addressbook data
expect(addressBookEntriesCount).toEqual(0)
expect(addressBook).toEqual(undefined)

expect(addedSafesCount).toEqual(2)
expect(addedSafes).toBeDefined()
if (!addedSafes) {
fail('No added safes found')
}
expect(addedSafes['5'][goerliSafeAddress]).toBeDefined()
const goerliAddedSafe = addedSafes['5'][goerliSafeAddress]
expect(goerliAddedSafe.threshold).toEqual(2)

expect(addedSafes['1'][mainnetSafeAddress]).toBeDefined()
const mainnetAddedSafe = addedSafes['1'][mainnetSafeAddress]
expect(mainnetAddedSafe.threshold).toEqual(1)
})

it('should parse address book entries correctly', () => {
const goerliAddress1 = '0xAecDFD3A19f777F0c03e6bf99AAfB59937d6467b'
const goerliName1 = 'test.eth'
const goerliAddress2 = '0x3819b800c67Be64029C1393c8b2e0d0d627dADE2'
const goerliName2 = 'some.eth'
const mainnetAddress1 = '0x954cD69f0E902439f99156e3eeDA080752c08401'
const mainnetName1 = 'mobile owner'
const mainnetAddress2 = '0x7cB6E6Cbc845e79d9CA05F6577141DA36ad398f5'
const mainnetName2 = 'S0mE&W3!rd#N4m€'

const jsonData = JSON.stringify({
version: '1.0',
data: {
SAFE__addressBook: `[{"address":"${mainnetAddress1}","name":"${mainnetName1}","chainId":"1"},
{"address":"${mainnetAddress2}","name":"${mainnetName2}","chainId":"1"},
{"address":"${goerliAddress1}","name":"${goerliName1}","chainId":"5"},
{"address":"${goerliAddress2}","name":"${goerliName2}","chainId":"5"}]`,
},
})

const { result } = renderHook(() => useGlobalImportJsonParser(jsonData))

const { addedSafes, addedSafesCount, addressBook, addressBookEntriesCount } = result.current

// no added safes
// No addressbook data
expect(addedSafesCount).toEqual(0)
expect(addedSafes).toEqual(undefined)

expect(addressBookEntriesCount).toEqual(4)
if (!addressBook) {
fail('No addressbook migrated')
}
expect(addressBook['5'][goerliAddress1]).toEqual(goerliName1)
expect(addressBook['5'][goerliAddress2]).toEqual(goerliName2)

expect(addressBook['1'][mainnetAddress1]).toEqual(mainnetName1)
expect(addressBook['1'][mainnetAddress2]).toEqual(mainnetName2)
})
})
Loading

0 comments on commit a26e5a7

Please sign in to comment.