Merge pull request #878 from gnosis/development

Apps v2.1.0
This commit is contained in:
Mati Dastugue 2020-05-12 17:18:07 -03:00 committed by GitHub
commit 8459af859f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
70 changed files with 2065 additions and 2096 deletions

View File

@ -6,6 +6,7 @@ on:
branches:
# this will run on the specified branch
- master
- development
env:
REACT_APP_BLOCKNATIVE_KEY: ${{ secrets.REACT_APP_BLOCKNATIVE_KEY }}
@ -14,7 +15,7 @@ env:
REACT_APP_INFURA_TOKEN: ${{ secrets.REACT_APP_INFURA_TOKEN }}
REACT_APP_PORTIS_ID: ${{ secrets.REACT_APP_PORTIS_ID }}
REACT_APP_GNOSIS_APPS_URL: ${{ secrets.REACT_APP_GNOSIS_APPS_URL }}
REACT_APP_INTERCOM_ID: ${{ secrets.REACT_APP_INTERCOM_ID }}
jobs:
release:
runs-on: ${{ matrix.os }}
@ -34,14 +35,12 @@ jobs:
shell: powershell
run: |
yarn global add --production windows-build-tools --vs2015 --msvs_version=2015
- name: Install node-gyp
if: startsWith(matrix.os, 'windows')
shell: powershell
run: |
yarn global add node-gyp
yarn config set node_gyp "$_\node_modules\node-gyp\bin\node-gyp.js"
- name: Install Node.js, NPM and Yarn
uses: actions/setup-node@v1
with:
@ -70,3 +69,25 @@ jobs:
# If the commit is tagged with a version (e.g. "v1.0.0"),
# release the app after building
release: ${{ startsWith(github.ref, 'refs/tags/v') }}
- name: 'Upload Artifacts OSX'
if: contains(github.ref, 'development') && startsWith(matrix.os, 'macos')
uses: actions/upload-artifact@v2
with:
name: Desktop OSX
path: ./dist/Safe[ ]Multisig*.dmg
- name: 'Upload Artifacts Linux'
if: contains(github.ref, 'development') && startsWith(matrix.os, 'ubuntu')
uses: actions/upload-artifact@v2
with:
name: Desktop Linux
path: ./dist/Safe[ ]Multisig*.AppImage
- name: 'Upload Artifacts Windows'
if: contains(github.ref, 'development') && startsWith(matrix.os, 'windows')
uses: actions/upload-artifact@v2
with:
name: Desktop Windows
path: ./dist/Safe[ ]Multisig*.exe

View File

@ -52,7 +52,7 @@ function ensureSlash(path, needsSlash) {
// single-page apps that may serve index.html for nested URLs like /todos/42.
// We can't use a relative path in HTML because we don't want to load something
// like /todos/42/static/js/bundle.7289d.js. We have to know the root.
const buildDesktop = process.env.BUILD_FOR_DESKTOP
const buildDesktop = process.env.REACT_APP_BUILD_FOR_DESKTOP
const homepagePath = require(paths.appPackageJson).homepage
// var homepagePathname = homepagePath ? url.parse(homepagePath).pathname : '/';

View File

@ -1,6 +1,6 @@
{
"name": "safe-react",
"version": "2.0.0",
"version": "2.1.0",
"description": "Allowing crypto users manage funds in a safer way",
"homepage": "https://github.com/gnosis/safe-react#readme",
"bugs": {
@ -25,7 +25,7 @@
"electron-dev": "concurrently \"BROWSER=none yarn start\" \"wait-on http://localhost:3000 && electron .\"",
"preelectron-pack": "yarn build",
"build-mainnet": "cross-env REACT_APP_NETWORK=mainnet yarn build",
"build-desktop": "cross-env BUILD_FOR_DESKTOP=true yarn build-mainnet",
"build-desktop": "cross-env REACT_APP_BUILD_FOR_DESKTOP=true yarn build-mainnet",
"flow": "flow",
"format:staged": "lint-staged",
"lint:check": "eslint './src/**/*.{js,jsx}'",
@ -48,11 +48,11 @@
"prettier --write"
]
},
"productName": "Safe Electron",
"productName": "Safe Multisig",
"build": {
"appId": "io.gnosis.safe.macos",
"afterSign": "scripts/notarize.js",
"productName": "Safe Electron",
"productName": "Safe Multisig",
"asar": true,
"publish": [
{
@ -136,27 +136,27 @@
},
"dependencies": {
"@gnosis.pm/safe-contracts": "1.1.1-dev.2",
"@gnosis.pm/util-contracts": "2.0.6",
"@gnosis.pm/safe-react-components": "https://github.com/gnosis/safe-react-components.git#a057248",
"@ledgerhq/hw-transport-node-hid": "5.12.0",
"@material-ui/core": "4.9.10",
"@gnosis.pm/util-contracts": "2.0.6",
"@ledgerhq/hw-transport-node-hid": "5.15.0",
"@material-ui/core": "4.9.14",
"@material-ui/icons": "4.9.1",
"@material-ui/lab": "4.0.0-alpha.39",
"@openzeppelin/contracts": "^2.5.0",
"@testing-library/jest-dom": "5.5.0",
"@welldone-software/why-did-you-render": "4.0.8",
"@openzeppelin/contracts": "3.0.1",
"@testing-library/jest-dom": "5.7.0",
"@welldone-software/why-did-you-render": "4.2.1",
"async-sema": "^3.1.0",
"axios": "0.19.2",
"bignumber.js": "9.0.0",
"bnc-onboard": "1.7.6",
"bnc-onboard": "1.9.0",
"connected-react-router": "6.8.0",
"cross-env": "^7.0.2",
"currency-flags": "^2.1.1",
"date-fns": "2.12.0",
"currency-flags": "2.1.2",
"date-fns": "2.13.0",
"dotenv": "^8.2.0",
"electron-is-dev": "^1.1.0",
"electron-log": "^4.1.1",
"electron-updater": "4.2.0",
"electron-log": "4.1.2",
"electron-updater": "4.3.1",
"ethereum-ens": "0.8.0",
"express": "^4.17.1",
"final-form": "4.19.1",
@ -165,13 +165,13 @@
"immutable": "^4.0.0-rc.9",
"install": "^0.13.0",
"js-cookie": "^2.2.1",
"lint-staged": "10.1.3",
"lint-staged": "10.2.2",
"material-ui-search-bar": "^1.0.0-beta.13",
"notistack": "https://github.com/gnosis/notistack.git#v0.9.4",
"npm": "^6.14.4",
"npm": "6.14.5",
"open": "^7.0.3",
"optimize-css-assets-webpack-plugin": "5.0.3",
"polished": "3.5.1",
"polished": "3.6.3",
"qrcode.react": "1.0.0",
"query-string": "6.12.1",
"react": "16.13.1",
@ -180,10 +180,10 @@
"react-final-form": "6.4.0",
"react-final-form-listeners": "^1.0.2",
"react-ga": "^2.7.0",
"react-hot-loader": "4.12.20",
"react-hot-loader": "4.12.21",
"react-qr-reader": "^2.2.1",
"react-redux": "7.2.0",
"react-router-dom": "5.1.2",
"react-router-dom": "5.2.0",
"react-window": "^1.8.5",
"recompose": "^0.30.0",
"redux": "4.0.5",
@ -192,8 +192,8 @@
"reselect": "^4.0.0",
"semver": "7.3.2",
"styled-components": "^5.0.1",
"wait-on": "^4.0.1",
"web3": "1.2.6"
"wait-on": "5.0.0",
"web3": "1.2.7"
},
"devDependencies": {
"@babel/cli": "7.8.4",
@ -220,13 +220,13 @@
"@babel/preset-env": "7.9.5",
"@babel/preset-flow": "7.9.0",
"@babel/preset-react": "7.9.4",
"@testing-library/react": "10.0.2",
"@testing-library/react": "10.0.3",
"autoprefixer": "9.7.6",
"babel-core": "^7.0.0-bridge.0",
"babel-eslint": "10.1.0",
"babel-jest": "25.3.0",
"babel-jest": "25.4.0",
"babel-loader": "8.1.0",
"babel-plugin-dynamic-import-node": "^2.3.0",
"babel-plugin-dynamic-import-node": "2.3.3",
"babel-plugin-transform-es3-member-expression-literals": "^6.22.0",
"babel-plugin-transform-es3-property-literals": "^6.22.0",
"babel-polyfill": "^6.26.0",
@ -239,37 +239,37 @@
"electron-builder": "22.2.0",
"electron-notarize": "^0.2.1",
"eslint": "^6.8.0",
"eslint-config-prettier": "6.10.1",
"eslint-config-prettier": "6.11.0",
"eslint-plugin-flowtype": "4.7.0",
"eslint-plugin-import": "2.20.2",
"eslint-plugin-jsx-a11y": "^6.2.3",
"eslint-plugin-prettier": "^3.1.2",
"eslint-plugin-react": "^7.18.3",
"eslint-plugin-sort-destructure-keys": "^1.3.3",
"eslint-plugin-sort-destructure-keys": "1.3.4",
"ethereumjs-abi": "0.6.8",
"extract-text-webpack-plugin": "^4.0.0-beta.0",
"file-loader": "6.0.0",
"flow-bin": "0.122.0",
"flow-bin": "0.123.0",
"fs-extra": "9.0.0",
"html-loader": "1.1.0",
"html-webpack-plugin": "4.2.0",
"husky": "^4.2.2",
"jest": "25.3.0",
"jest": "25.4.0",
"jest-dom": "4.0.0",
"json-loader": "^0.5.7",
"mini-css-extract-plugin": "0.9.0",
"postcss-loader": "^3.0.0",
"postcss-mixins": "6.2.3",
"postcss-simple-vars": "^5.0.2",
"prettier": "2.0.4",
"prettier": "2.0.5",
"run-with-testrpc": "0.3.1",
"style-loader": "1.1.4",
"terser-webpack-plugin": "2.3.5",
"truffle": "5.1.21",
"truffle": "5.1.23",
"truffle-contract": "4.0.31",
"truffle-solidity-loader": "0.1.32",
"url-loader": "4.1.0",
"webpack": "4.42.1",
"webpack": "4.43.0",
"webpack-bundle-analyzer": "3.7.0",
"webpack-cli": "3.3.11",
"webpack-dev-server": "3.10.3",

View File

@ -129,14 +129,18 @@ function createWindow() {
autoUpdater.init(mainWindow);
});
mainWindow.webContents.on('crashed', () => {
log.info('App Crashed');
mainWindow.webContents.on('crashed', (event) => {
log.info(`App Crashed: ${event}`);
mainWindow.reload();
});
mainWindow.on("closed", () => (mainWindow = null));
}
process.on('uncaughtException',function(error){
log.error(error);
});
app.userAgentFallback = process.platform ==='win32' ?
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.100 Safari/537.36' :
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) old-airport-include/1.0.0 Chrome Electron/7.1.7 Safari/537.36';
@ -144,7 +148,7 @@ app.userAgentFallback = process.platform ==='win32' ?
app.commandLine.appendSwitch('ignore-certificate-errors');
app.on("ready", () =>{
// Hide the menu
//Menu.setApplicationMenu(null);
Menu.setApplicationMenu(null);
if(!isDev) createServer();
createWindow();
});

View File

@ -2,11 +2,21 @@
// It has the same sandbox as a Chrome extension.
const TransportNodeHid = require("@ledgerhq/hw-transport-node-hid").default;
const log = require('electron-log');
window.TransportNodeHid = TransportNodeHid;
window.isDesktop = true;
window.addEventListener('DOMContentLoaded', () => {
console.error = (...args) => {
log.error(...args)
}
console.warn = (...args) => {
log.warn(...args)
}
console.log = (...args) => {
log.info(...args)
}
const replaceText = (selector, text) => {
const element = document.getElementById(selector)
if (element) element.innerText = text

View File

@ -28,6 +28,22 @@ export const Menu = styled.div.attrs(() => ({ className: 'background' }))`
border-top-left-radius: 8px;
border-bottom-left-radius: 8px;
background-color: white;
overflow-y: auto;
::-webkit-scrollbar {
width: 0.7em !important;
scroll-behavior: smooth !important;
}
::-webkit-scrollbar-track {
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3) !important;
}
::-webkit-scrollbar-thumb {
background-color: darkgrey !important;
outline: 1px solid slategrey !important;
border-radius: 10px !important;
}
`
export const Content = styled.div.attrs(() => ({ className: 'background' }))`

View File

@ -17,6 +17,8 @@ import { mainFontFamily, md, primary, screenSm } from '~/theme/variables'
import { loadGoogleAnalytics } from '~/utils/googleAnalytics'
import { loadIntercom } from '~/utils/intercom'
const isDesktop = process.env.REACT_APP_BUILD_FOR_DESKTOP
const useStyles = makeStyles({
container: {
backgroundColor: '#fff',
@ -111,14 +113,18 @@ const CookiesBanner = () => {
fetchCookiesFromStorage()
}, [showBanner])
useEffect(() => {
if (isDesktop && showBanner) acceptCookiesHandler()
}, [isDesktop, showBanner])
const acceptCookiesHandler = async () => {
const newState = {
acceptedNecessary: true,
acceptedAnalytics: true,
acceptedAnalytics: !isDesktop,
}
await saveCookie(COOKIES_KEY, newState, 365)
dispatch(openCookieBanner(false))
setShowAnalytics(true)
setShowAnalytics(!isDesktop)
}
const closeCookiesBannerHandler = async () => {
@ -193,8 +199,9 @@ const CookiesBanner = () => {
loadIntercom()
loadGoogleAnalytics()
}
if (isDesktop) loadIntercom()
return showBanner ? cookieBannerContent : null
return showBanner && !isDesktop ? cookieBannerContent : null
}
export default CookiesBanner

View File

@ -29,6 +29,7 @@ type Props = {
userAddress: string,
classes: Object,
onDisconnect: Function,
openDashboard?: Function,
}
const styles = () => ({
@ -72,6 +73,15 @@ const styles = () => ({
},
disconnect: {
padding: `${md} ${lg}`,
'& button': {
background: '#f02525',
},
},
dashboard: {
padding: `${md} ${lg} ${xs}`,
},
dashboardText: {
letterSpacing: '1px',
},
disconnectText: {
letterSpacing: '1px',
@ -92,7 +102,7 @@ const styles = () => ({
},
})
const UserDetails = ({ classes, connected, network, onDisconnect, provider, userAddress }: Props) => {
const UserDetails = ({ classes, connected, network, onDisconnect, openDashboard, provider, userAddress }: Props) => {
const status = connected ? 'Connected' : 'Connection error'
const address = userAddress ? shortVersionOf(userAddress, 4) : 'Address not available'
const identiconAddress = userAddress || 'random'
@ -154,6 +164,15 @@ const UserDetails = ({ classes, connected, network, onDisconnect, provider, user
</Paragraph>
</Row>
<Hairline margin="xs" />
{openDashboard && (
<Row className={classes.dashboard}>
<Button color="primary" fullWidth onClick={openDashboard} size="medium" variant="contained">
<Paragraph className={classes.dashboardText} color="white" noMargin size="md">
{upperFirst(provider)} Wallet
</Paragraph>
</Button>
</Row>
)}
<Row className={classes.disconnect}>
<Button color="primary" fullWidth onClick={onDisconnect} size="medium" variant="contained">
<Paragraph className={classes.disconnectText} color="white" noMargin size="md">

View File

@ -54,6 +54,10 @@ class HeaderComponent extends React.PureComponent<Props, State> {
logComponentStack(error, info)
}
getOpenDashboard = () => {
const { wallet } = onboard.getState()
return wallet.type === 'sdk' && wallet.dashboard
}
onDisconnect = () => {
const { closeSnackbar, enqueueSnackbar, removeProvider } = this.props
@ -84,6 +88,7 @@ class HeaderComponent extends React.PureComponent<Props, State> {
connected={available}
network={network}
onDisconnect={this.onDisconnect}
openDashboard={this.getOpenDashboard()}
provider={provider}
userAddress={userAddress}
/>

View File

@ -1,6 +1,10 @@
/* Onboard.js custom styles */
:global(.bn-onboard-custom.bn-onboard-modal) {
font-family: "Averta";
font-family: 'Averta';
z-index: 2001;
}
:global(.torusIframe) {
z-index: 9999;
}

View File

@ -0,0 +1,51 @@
// @flow
import { makeStyles } from '@material-ui/core/styles'
import { useState } from 'react'
import * as React from 'react'
import QRIcon from '~/assets/icons/qrcode.svg'
import ScanQRModal from '~/components/ScanQRModal'
import Img from '~/components/layout/Img'
type Props = {
handleScan: Function,
}
const useStyles = makeStyles({
qrCodeBtn: {
cursor: 'pointer',
},
})
export const ScanQRWrapper = (props: Props) => {
const classes = useStyles()
const [qrModalOpen, setQrModalOpen] = useState<boolean>(false)
const openQrModal = () => {
setQrModalOpen(true)
}
const closeQrModal = () => {
setQrModalOpen(false)
}
const onScanFinished = (value) => {
props.handleScan(value, closeQrModal)
}
return (
<>
<Img
alt="Scan QR"
className={classes.qrCodeBtn}
height={20}
onClick={() => {
openQrModal()
}}
role="button"
src={QRIcon}
/>
{qrModalOpen && <ScanQRModal isOpen={qrModalOpen} onClose={closeQrModal} onScan={onScanFinished} />}
</>
)
}

View File

@ -12,6 +12,7 @@ import { REMOVE_ENTRY } from '~/logic/addressBook/store/actions/removeAddressBoo
import { UPDATE_ENTRY } from '~/logic/addressBook/store/actions/updateAddressBookEntry'
import { getAddressesListFromAdbk } from '~/logic/addressBook/utils'
import { sameAddress } from '~/logic/wallets/ethAddresses'
import { checksumAddress } from '~/utils/checksumAddress'
export const ADDRESS_BOOK_REDUCER_ID = 'addressBook'
@ -20,7 +21,7 @@ export type State = Map<string, Map<string, AddressBookEntry>>
export const buildAddressBook = (storedAdbk: AddressBook): AddressBookProps => {
let addressBookBuilt = Map([])
Object.entries(storedAdbk).forEach((adbkProps: Array<string, AddressBookEntry[]>) => {
const safeAddress = adbkProps[0]
const safeAddress = checksumAddress(adbkProps[0])
const adbkRecords = adbkProps[1].map(makeAddressBookEntry)
const adbkSafeEntries = List(adbkRecords)
addressBookBuilt = addressBookBuilt.set(safeAddress, adbkSafeEntries)

View File

@ -34,3 +34,16 @@ export const getAddressBookListSelector: Selector<GlobalState, {}, List<AddressB
return result
},
)
export const getNameFromAddressBook = createSelector(
getAddressBookListSelector,
(_, address) => address,
(addressBook: Map<string, AddressBook>, address: string) => {
const adbkEntry = addressBook.find((addressBookItem) => addressBookItem.address === address)
if (adbkEntry) {
return adbkEntry.name
}
return 'UNKNOWN'
},
)

View File

@ -1,8 +1,5 @@
// @flow
import { useSelector } from 'react-redux'
import type { AddressBook, AddressBookProps } from '~/logic/addressBook/model/addressBook'
import { getAddressBook } from '~/logic/addressBook/store/selectors'
import type { Owner } from '~/routes/safe/store/models/owner'
import { loadFromStorage, saveToStorage } from '~/utils/storage'
@ -25,7 +22,7 @@ export const saveAddressBook = async (addressBook: AddressBook) => {
export const getAddressesListFromAdbk = (addressBook: AddressBook) =>
Array.from(addressBook).map((entry) => entry.address)
const getNameFromAdbk = (addressBook: AddressBook, userAddress: string): string | null => {
export const getNameFromAdbk = (addressBook: AddressBook, userAddress: string): string | null => {
const entry = addressBook.find((addressBookItem) => addressBookItem.address === userAddress)
if (entry) {
return entry.name
@ -33,14 +30,6 @@ const getNameFromAdbk = (addressBook: AddressBook, userAddress: string): string
return null
}
export const getNameFromAddressBook = (userAddress: string): string | null => {
if (!userAddress) {
return null
}
const addressBook = useSelector(getAddressBook)
return addressBook ? getNameFromAdbk(addressBook, userAddress) : null
}
export const getOwnersWithNameFromAddressBook = (addressBook: AddressBook, ownerList: List<Owner>) => {
if (!ownerList) {
return []

View File

@ -7,15 +7,15 @@ import { AVAILABLE_CURRENCIES } from '~/logic/currencyValues/store/model/currenc
import type { GlobalState } from '~/store'
// eslint-disable-next-line max-len
const fetchCurrencySelectedValue = (currencyValueSelected: $Keys<typeof AVAILABLE_CURRENCIES>) => async (
const fetchCurrencyRate = (safeAddress: string, selectedCurrency: $Keys<typeof AVAILABLE_CURRENCIES>) => async (
dispatch: ReduxDispatch<GlobalState>,
) => {
if (AVAILABLE_CURRENCIES.USD === currencyValueSelected) {
return dispatch(setCurrencyRate('1'))
if (AVAILABLE_CURRENCIES.USD === selectedCurrency) {
return dispatch(setCurrencyRate(safeAddress, 1))
}
const selectedCurrencyRateInBaseCurrency = await fetchCurrenciesRates(AVAILABLE_CURRENCIES.USD, currencyValueSelected)
dispatch(setCurrencyRate(selectedCurrencyRateInBaseCurrency))
const selectedCurrencyRateInBaseCurrency = await fetchCurrenciesRates(AVAILABLE_CURRENCIES.USD, selectedCurrency)
dispatch(setCurrencyRate(safeAddress, selectedCurrencyRateInBaseCurrency))
}
export default fetchCurrencySelectedValue
export default fetchCurrencyRate

View File

@ -1,36 +1,42 @@
// @flow
import { List } from 'immutable'
import { batch } from 'react-redux'
import type { Dispatch as ReduxDispatch } from 'redux'
import fetchCurrencySelectedValue from '~/logic/currencyValues/store/actions/fetchCurrencySelectedValue'
import { CURRENCY_SELECTED_KEY } from '~/logic/currencyValues/store/actions/saveCurrencySelected'
import fetchCurrencyRate from '~/logic/currencyValues/store/actions/fetchCurrencyRate'
import { setCurrencyBalances } from '~/logic/currencyValues/store/actions/setCurrencyBalances'
import { setCurrencyRate } from '~/logic/currencyValues/store/actions/setCurrencyRate'
import { setCurrencySelected } from '~/logic/currencyValues/store/actions/setCurrencySelected'
import { setSelectedCurrency } from '~/logic/currencyValues/store/actions/setSelectedCurrency'
import { AVAILABLE_CURRENCIES } from '~/logic/currencyValues/store/model/currencyValues'
import { loadCurrencyValues } from '~/logic/currencyValues/store/utils/currencyValuesStorage'
import fetchSafeTokens from '~/logic/tokens/store/actions/fetchSafeTokens'
import type { GlobalState } from '~/store'
import { loadFromStorage } from '~/utils/storage'
export const fetchCurrencyValues = () => async (dispatch: ReduxDispatch<GlobalState>) => {
export const fetchCurrencyValues = (safeAddress: string) => async (dispatch: ReduxDispatch<GlobalState>) => {
try {
const currencyStored = await loadFromStorage(CURRENCY_SELECTED_KEY)
if (!currencyStored) {
const storedCurrencies = await loadCurrencyValues()
const storedCurrency = storedCurrencies[safeAddress]
if (!storedCurrency) {
return batch(() => {
dispatch(setCurrencySelected(AVAILABLE_CURRENCIES.USD))
dispatch(setCurrencyRate(1))
dispatch(setCurrencyBalances(safeAddress, List([])))
dispatch(setSelectedCurrency(safeAddress, AVAILABLE_CURRENCIES.USD))
dispatch(setCurrencyRate(safeAddress, 1))
})
}
const { currencyValueSelected } = currencyStored
// Loads the stored state on redux
Object.entries(storedCurrencies).forEach((kv) => {
const safeAddr = kv[0]
const value = kv[1]
const { currencyRate, selectedCurrency } = value
batch(() => {
dispatch(setCurrencySelected(currencyValueSelected))
dispatch(fetchCurrencySelectedValue(currencyValueSelected))
dispatch(setSelectedCurrency(safeAddr, selectedCurrency))
dispatch(setCurrencyRate(safeAddr, currencyRate))
dispatch(fetchCurrencyRate(safeAddr, selectedCurrency))
dispatch(fetchSafeTokens(safeAddress))
})
})
} catch (err) {
console.error('Error fetching tokens price list', err)
console.error('Error fetching currency values', err)
}
return Promise.resolve()
}
export default fetchCurrencyValues

View File

@ -1,19 +0,0 @@
// @flow
import { Dispatch as ReduxDispatch } from 'redux'
import { setCurrencySelected } from '~/logic/currencyValues/store/actions/setCurrencySelected'
import { AVAILABLE_CURRENCIES } from '~/logic/currencyValues/store/model/currencyValues'
import type { GlobalState } from '~/store'
import { saveToStorage } from '~/utils/storage'
export const CURRENCY_SELECTED_KEY = 'CURRENCY_SELECTED_KEY'
const saveCurrencySelected = (currencySelected: AVAILABLE_CURRENCIES) => async (
dispatch: ReduxDispatch<GlobalState>,
) => {
await saveToStorage(CURRENCY_SELECTED_KEY, { currencyValueSelected: currencySelected })
dispatch(setCurrencySelected(currencySelected))
}
export default saveCurrencySelected

View File

@ -1,5 +1,4 @@
// @flow
import { Map } from 'immutable'
import { createAction } from 'redux-actions'
import type { CurrencyValues, CurrencyValuesProps } from '~/logic/currencyValues/store/model/currencyValues'
@ -9,5 +8,8 @@ export const SET_CURRENCY_BALANCES = 'SET_CURRENCY_BALANCES'
// eslint-disable-next-line max-len
export const setCurrencyBalances = createAction<string, *>(
SET_CURRENCY_BALANCES,
(currencyBalances: Map<string, CurrencyValues>): CurrencyValuesProps => ({ currencyBalances }),
(safeAddress: string, currencyBalances: List<CurrencyValues>): CurrencyValuesProps => ({
safeAddress,
currencyBalances,
}),
)

View File

@ -8,5 +8,5 @@ export const SET_CURRENCY_RATE = 'SET_CURRENCY_RATE'
// eslint-disable-next-line max-len
export const setCurrencyRate = createAction<string, *>(
SET_CURRENCY_RATE,
(currencyRate: string): CurrencyValuesProps => ({ currencyRate }),
(safeAddress: string, currencyRate: string): CurrencyValuesProps => ({ safeAddress, currencyRate }),
)

View File

@ -7,7 +7,10 @@ import { AVAILABLE_CURRENCIES } from '~/logic/currencyValues/store/model/currenc
export const SET_CURRENT_CURRENCY = 'SET_CURRENT_CURRENCY'
// eslint-disable-next-line max-len
export const setCurrencySelected = createAction<string, *>(
export const setSelectedCurrency = createAction<string, *>(
SET_CURRENT_CURRENCY,
(currencyValueSelected: $Keys<typeof AVAILABLE_CURRENCIES>): CurrencyValuesProps => ({ currencyValueSelected }),
(safeAddress: string, selectedCurrency: $Keys<typeof AVAILABLE_CURRENCIES>): CurrencyValuesProps => ({
safeAddress,
selectedCurrency,
}),
)

View File

@ -0,0 +1,50 @@
// @flow
import { Action, Store } from 'redux'
import fetchCurrencyRate from '~/logic/currencyValues/store/actions/fetchCurrencyRate'
import { SET_CURRENCY_BALANCES } from '~/logic/currencyValues/store/actions/setCurrencyBalances'
import { SET_CURRENCY_RATE } from '~/logic/currencyValues/store/actions/setCurrencyRate'
import { SET_CURRENT_CURRENCY } from '~/logic/currencyValues/store/actions/setSelectedCurrency'
import { currencyValuesSelector } from '~/logic/currencyValues/store/selectors'
import { saveCurrencyValues } from '~/logic/currencyValues/store/utils/currencyValuesStorage'
import type { GlobalState } from '~/routes/safe/store/middleware/safeStorage'
const watchedActions = [SET_CURRENT_CURRENCY, SET_CURRENCY_RATE, SET_CURRENCY_BALANCES]
const currencyValuesStorageMiddleware = (store: Store<GlobalState>) => (next: Function) => async (
action: Action<*>,
) => {
const handledAction = next(action)
if (watchedActions.includes(action.type)) {
const state: GlobalState = store.getState()
const { dispatch } = store
switch (action.type) {
case SET_CURRENT_CURRENCY: {
const { safeAddress, selectedCurrency } = action.payload
dispatch(fetchCurrencyRate(safeAddress, selectedCurrency))
break
}
case SET_CURRENCY_RATE:
case SET_CURRENCY_BALANCES: {
const currencyValues = currencyValuesSelector(state)
const currencyValuesWithoutBalances = currencyValues.map((currencyValue) => {
const currencyRate = currencyValue.get('currencyRate')
const selectedCurrency = currencyValue.get('selectedCurrency')
return {
currencyRate,
selectedCurrency,
}
})
await saveCurrencyValues(currencyValuesWithoutBalances)
break
}
default:
break
}
}
return handledAction
}
export default currencyValuesStorageMiddleware

View File

@ -1,5 +1,5 @@
// @flow
import type { RecordOf } from 'immutable'
import type { RecordFactory, RecordOf } from 'immutable'
import { Record } from 'immutable'
export const AVAILABLE_CURRENCIES = {
@ -45,17 +45,22 @@ export type BalanceCurrencyType = {
balanceInSelectedCurrency: string,
}
export const makeBalanceCurrency = Record({
export const makeBalanceCurrency: RecordFactory<BalanceCurrencyType> = Record({
currencyName: '',
tokenAddress: '',
balanceInBaseCurrency: '',
balanceInSelectedCurrency: '',
})
export type CurrencyValuesProps = {
currencyValueSelected: $Keys<typeof AVAILABLE_CURRENCIES>,
currencyRate: string,
export type CurrencyValuesEntry = {
selectedCurrency: $Keys<typeof AVAILABLE_CURRENCIES>,
currencyRate: number,
currencyValuesList: BalanceCurrencyType[],
}
export type CurrencyValuesProps = {
// Map safe address to currency values entry
currencyValues: Map<string, CurrencyValuesEntry>,
}
export type CurrencyValues = RecordOf<CurrencyValuesProps>

View File

@ -4,7 +4,7 @@ import { type ActionType, handleActions } from 'redux-actions'
import { SET_CURRENCY_BALANCES } from '~/logic/currencyValues/store/actions/setCurrencyBalances'
import { SET_CURRENCY_RATE } from '~/logic/currencyValues/store/actions/setCurrencyRate'
import { SET_CURRENT_CURRENCY } from '~/logic/currencyValues/store/actions/setCurrencySelected'
import { SET_CURRENT_CURRENCY } from '~/logic/currencyValues/store/actions/setSelectedCurrency'
import type { State } from '~/logic/tokens/store/reducer/tokens'
export const CURRENCY_VALUES_KEY = 'currencyValues'
@ -12,19 +12,19 @@ export const CURRENCY_VALUES_KEY = 'currencyValues'
export default handleActions<State, *>(
{
[SET_CURRENCY_RATE]: (state: State, action: ActionType<Function>): State => {
const { currencyRate } = action.payload
const { currencyRate, safeAddress } = action.payload
return state.set('currencyRate', currencyRate)
return state.setIn([safeAddress, 'currencyRate'], currencyRate)
},
[SET_CURRENCY_BALANCES]: (state: State, action: ActionType<Function>): State => {
const { currencyBalances } = action.payload
const { currencyBalances, safeAddress } = action.payload
return state.set('currencyBalances', currencyBalances)
return state.setIn([safeAddress, 'currencyBalances'], currencyBalances)
},
[SET_CURRENT_CURRENCY]: (state: State, action: ActionType<Function>): State => {
const { currencyValueSelected } = action.payload
const { safeAddress, selectedCurrency } = action.payload
return state.set('currencyValueSelected', currencyValueSelected)
return state.setIn([safeAddress, 'selectedCurrency'], selectedCurrency)
},
},
Map(),

View File

@ -1,13 +1,38 @@
// @flow
import { List } from 'immutable'
import { type OutputSelector, createSelector } from 'reselect'
import type { CurrencyValuesEntry, CurrencyValuesProps } from '~/logic/currencyValues/store/model/currencyValues'
import { CURRENCY_VALUES_KEY } from '~/logic/currencyValues/store/reducer/currencyValues'
import { safeParamAddressFromStateSelector } from '~/routes/safe/store/selectors'
import { type GlobalState } from '~/store'
export const currencyValuesListSelector = (state: GlobalState) =>
state[CURRENCY_VALUES_KEY].get('currencyBalances') ? state[CURRENCY_VALUES_KEY].get('currencyBalances') : List([])
export const currencyValuesSelector = (state: GlobalState): CurrencyValuesEntry => state[CURRENCY_VALUES_KEY]
export const currentCurrencySelector = (state: GlobalState) => state[CURRENCY_VALUES_KEY].get('currencyValueSelected')
export const safeFiatBalancesSelector: OutputSelector<GlobalState> = createSelector(
currencyValuesSelector,
safeParamAddressFromStateSelector,
(currencyValues: CurrencyValuesProps, safeAddress: string) => {
if (!currencyValues) return
return currencyValues.get(safeAddress)
},
)
export const currencyRateSelector = (state: GlobalState) => state[CURRENCY_VALUES_KEY].get('currencyRate')
export const safeFiatBalancesListSelector: OutputSelector<GlobalState> = createSelector(
safeFiatBalancesSelector,
(currencyValuesMap: CurrencyValuesProps) => {
if (!currencyValuesMap) return
return currencyValuesMap.get('currencyBalances') ? currencyValuesMap.get('currencyBalances') : List([])
},
)
export const currentCurrencySelector: OutputSelector<GlobalState> = createSelector(
safeFiatBalancesSelector,
(currencyValuesMap?: CurrencyValuesProps) => (currencyValuesMap ? currencyValuesMap.get('selectedCurrency') : null),
)
export const currencyRateSelector: OutputSelector<GlobalState> = createSelector(
safeFiatBalancesSelector,
(currencyValuesMap: CurrencyValuesProps) => (currencyValuesMap ? currencyValuesMap.get('currencyRate') : null),
)

View File

@ -0,0 +1,18 @@
// @flow
import { Map } from 'immutable'
import type { CurrencyValuesEntry } from '~/logic/currencyValues/store/model/currencyValues'
import { loadFromStorage, saveToStorage } from '~/utils/storage'
const CURRENCY_VALUES_STORAGE_KEY = 'CURRENCY_VALUES_STORAGE_KEY'
export const saveCurrencyValues = async (currencyValues: Map<string, CurrencyValuesEntry>) => {
try {
await saveToStorage(CURRENCY_VALUES_STORAGE_KEY, currencyValues)
} catch (err) {
console.error('Error storing currency values info in localstorage', err)
}
}
export const loadCurrencyValues = async () => {
return (await loadFromStorage(CURRENCY_VALUES_STORAGE_KEY)) || {}
}

View File

@ -12,7 +12,7 @@ export const generateSignaturesFromTxConfirmations = (
// The constant parts need to be sorted so that the recovered signers are sorted ascending
// (natural order) by address (not checksummed).
const confirmationsMap = confirmations.reduce((map, obj) => {
map[obj.owner.address.toLowerCase()] = obj // eslint-disable-line no-param-reassign
map[obj.owner.toLowerCase()] = obj // eslint-disable-line no-param-reassign
return map
}, {})

View File

@ -27,9 +27,7 @@ export const getAwaitingTransactions = (
if (!transaction.executionTxHash && !isTransactionCancelled) {
// Then we check if the waiting confirmations are not from the current user, otherwise, filters this
// transaction
const transactionWaitingUser = transaction.confirmations.filter(
(confirmation) => confirmation.owner && confirmation.owner.address !== userAccount,
)
const transactionWaitingUser = transaction.confirmations.filter(({ owner }) => owner !== userAccount)
return transactionWaitingUser.size > 0
}

View File

@ -1,10 +1,10 @@
// @flow
import { getIncomingTxServiceUriTo, getTxServiceHost } from '~/config'
import { getWeb3 } from '~/logic/wallets/getWeb3'
import { checksumAddress } from '~/utils/checksumAddress'
export const buildIncomingTxServiceUrl = (safeAddress: string) => {
const host = getTxServiceHost()
const address = getWeb3().utils.toChecksumAddress(safeAddress)
const address = checksumAddress(safeAddress)
const base = getIncomingTxServiceUriTo(address)
return `${host}${base}`

View File

@ -2,7 +2,7 @@
import axios from 'axios'
import { getTxServiceHost, getTxServiceUriFrom } from '~/config'
import { getWeb3 } from '~/logic/wallets/getWeb3'
import { checksumAddress } from '~/utils/checksumAddress'
export type TxServiceType = 'confirmation' | 'execution' | 'initialised'
export type Operation = 0 | 1 | 2
@ -38,7 +38,7 @@ const calculateBodyFrom = async (
)
return {
to: getWeb3().utils.toChecksumAddress(to),
to: checksumAddress(to),
value: valueInWei,
data,
operation,
@ -50,7 +50,7 @@ const calculateBodyFrom = async (
refundReceiver,
contractTransactionHash,
transactionHash,
sender: getWeb3().utils.toChecksumAddress(sender),
sender: checksumAddress(sender),
origin,
signature,
}
@ -58,7 +58,7 @@ const calculateBodyFrom = async (
export const buildTxServiceUrl = (safeAddress: string) => {
const host = getTxServiceHost()
const address = getWeb3().utils.toChecksumAddress(safeAddress)
const address = checksumAddress(safeAddress)
const base = getTxServiceUriFrom(address)
return `${host}${base}`
}

View File

@ -2,7 +2,6 @@
import type { Dispatch as ReduxDispatch } from 'redux'
import fetchCollectibles from '~/logic/collectibles/store/actions/fetchCollectibles'
import { nftAssetsSelector } from '~/logic/collectibles/store/selectors'
import updateActiveAssets from '~/routes/safe/store/actions/updateActiveAssets'
import {
@ -24,7 +23,6 @@ const activateAssetsByBalance = (safeAddress: string) => async (
return
}
await dispatch(fetchCollectibles())
const availableAssets = nftAssetsSelector(state)
const alreadyActiveAssets = safeActiveAssetsSelectorBySafe(safeAddress, safes)
const blacklistedAssets = safeBlacklistedAssetsSelectorBySafe(safeAddress, safes)

View File

@ -35,7 +35,6 @@ const fetchSafeTokens = (safeAddress: string) => async (dispatch: ReduxDispatch<
const alreadyActiveTokens = safe.get('activeTokens')
const blacklistedTokens = safe.get('blacklistedTokens')
const currencyValues = state[CURRENCY_VALUES_KEY]
const storedCurrencyBalances = currencyValues.get('currencyBalances')
const { balances, currencyList, ethBalance, tokens } = result.data.reduce(
(acc, { balance, balanceUsd, token, tokenAddress }) => {
@ -78,8 +77,14 @@ const fetchSafeTokens = (safeAddress: string) => async (dispatch: ReduxDispatch<
const updateActiveTokens = activeTokens.equals(alreadyActiveTokens) ? noFunc : update({ activeTokens })
const updateBalances = balances.equals(safeBalances) ? noFunc : update({ balances })
const updateEthBalance = ethBalance === currentEthBalance ? noFunc : update({ ethBalance })
const storedCurrencyBalances =
currencyValues && currencyValues.get(safeAddress)
? currencyValues.get(safeAddress).get('currencyBalances')
: undefined
const updateCurrencies = currencyList.equals(storedCurrencyBalances) ? noFunc : setCurrencyBalances(currencyList)
const updateCurrencies = currencyList.equals(storedCurrencyBalances)
? noFunc
: setCurrencyBalances(safeAddress, currencyList)
const updateTokens = tokens.size === 0 ? noFunc : addTokens(tokens)

View File

@ -23,7 +23,7 @@ const wallets = [
appUrl: 'gnosis-safe.io',
preferred: true,
email: 'safe@gnosis.io',
desktop: true,
desktop: false,
rpcUrl: infuraUrl,
},
{
@ -43,7 +43,6 @@ const wallets = [
{
walletName: 'portis',
apiKey: PORTIS_DAPP_ID,
label: 'Login with Email',
desktop: true,
},
{ walletName: 'authereum', desktop: false },

View File

@ -4,12 +4,14 @@ import { withStyles } from '@material-ui/core/styles'
import CheckCircle from '@material-ui/icons/CheckCircle'
import * as React from 'react'
import { ScanQRWrapper } from '~/components/ScanQRModal/ScanQRWrapper'
import OpenPaper from '~/components/Stepper/OpenPaper'
import AddressInput from '~/components/forms/AddressInput'
import Field from '~/components/forms/Field'
import TextField from '~/components/forms/TextField'
import { mustBeEthereumAddress, noErrorsOn, required } from '~/components/forms/validator'
import Block from '~/components/layout/Block'
import Col from '~/components/layout/Col'
import Paragraph from '~/components/layout/Paragraph'
import { SAFE_MASTER_COPY_ADDRESS_V10, getSafeMasterContract, validateProxy } from '~/logic/contracts/safeContracts'
import { getWeb3 } from '~/logic/wallets/getWeb3'
@ -80,18 +82,24 @@ export const safeFieldsValidation = async (values: Object) => {
return errors
}
const Details = ({ classes, errors, form }: Props) => (
const Details = ({ classes, errors, form }: Props) => {
const handleScan = (value, closeQrModal) => {
form.mutators.setValue(FIELD_LOAD_ADDRESS, value)
closeQrModal()
}
return (
<>
<Block margin="md">
<Paragraph color="primary" noMargin size="md">
You are about to load an existing Gnosis Safe. First, choose a name and enter the Safe address. The name is only
stored locally and will never be shared with Gnosis or any third parties.
You are about to load an existing Gnosis Safe. First, choose a name and enter the Safe address. The name is
only stored locally and will never be shared with Gnosis or any third parties.
<br />
Your connected wallet does not have to be the owner of this Safe. In this case, the interface will provide you a
read-only view.
Your connected wallet does not have to be the owner of this Safe. In this case, the interface will provide you
a read-only view.
</Paragraph>
</Block>
<Block className={classes.root}>
<Col xs={11}>
<Field
component={TextField}
name={FIELD_LOAD_NAME}
@ -100,8 +108,10 @@ const Details = ({ classes, errors, form }: Props) => (
type="text"
validate={required}
/>
</Col>
</Block>
<Block className={classes.root} margin="lg">
<Col xs={11}>
<AddressInput
component={TextField}
fieldMutator={(val) => {
@ -121,6 +131,10 @@ const Details = ({ classes, errors, form }: Props) => (
text="Safe Address"
type="text"
/>
</Col>
<Col center="xs" className={classes} middle="xs" xs={1}>
<ScanQRWrapper handleScan={handleScan} />
</Col>
</Block>
<Block margin="sm">
<Paragraph className={classes.links} color="primary" noMargin size="md">
@ -138,6 +152,7 @@ const Details = ({ classes, errors, form }: Props) => (
</Block>
</>
)
}
const DetailsForm = withStyles(styles)(Details)

View File

@ -252,12 +252,17 @@ const SafeDeployment = ({ creationTxHash, onCancel, onRetry, onSuccess, provider
const isTxMined = async (txHash) => {
const web3 = getWeb3()
const txResult = await web3.eth.getTransaction(txHash)
if (txResult.blockNumber === null) {
return false
}
const receipt = await web3.eth.getTransactionReceipt(txHash)
if (!receipt.status) {
throw Error('TX status reverted')
}
const txResult = await web3.eth.getTransaction(txHash)
return txResult.blockNumber !== null
return true
}
let interval = setInterval(async () => {

View File

@ -8,6 +8,7 @@ import { useSelector } from 'react-redux'
import { styles } from './style'
import Modal from '~/components/Modal'
import { ScanQRWrapper } from '~/components/ScanQRModal/ScanQRWrapper'
import AddressInput from '~/components/forms/AddressInput'
import Field from '~/components/forms/Field'
import GnoForm from '~/components/forms/GnoForm'
@ -15,6 +16,7 @@ import TextField from '~/components/forms/TextField'
import { composeValidators, minMaxLength, required, uniqueAddress } from '~/components/forms/validator'
import Block from '~/components/layout/Block'
import Button from '~/components/layout/Button'
import Col from '~/components/layout/Col'
import Hairline from '~/components/layout/Hairline'
import Paragraph from '~/components/layout/Paragraph'
import Row from '~/components/layout/Row'
@ -81,10 +83,21 @@ const CreateEditEntryModalComponent = ({
<GnoForm formMutators={formMutators} onSubmit={onFormSubmitted}>
{(...args) => {
const mutators = args[3]
const handleScan = (value, closeQrModal) => {
let scannedAddress = value
if (scannedAddress.startsWith('ethereum:')) {
scannedAddress = scannedAddress.replace('ethereum:', '')
}
mutators.setOwnerAddress(scannedAddress)
closeQrModal()
}
return (
<>
<Block className={classes.container}>
<Row margin="md">
<Col xs={11}>
<Field
className={classes.addressInput}
component={TextField}
@ -96,8 +109,10 @@ const CreateEditEntryModalComponent = ({
type="text"
validate={composeValidators(required, minMaxLength(1, 50))}
/>
</Col>
</Row>
<Row margin="md">
<Col xs={11}>
<AddressInput
className={classes.addressInput}
defaultValue={entryToEdit ? entryToEdit.entry.address : undefined}
@ -109,6 +124,12 @@ const CreateEditEntryModalComponent = ({
text="Owner address*"
validators={entryToEdit ? undefined : [entryDoesntExist]}
/>
</Col>
{!entryToEdit ? (
<Col center="xs" className={classes} middle="xs" xs={1}>
<ScanQRWrapper handleScan={handleScan} />
</Col>
) : null}
</Row>
</Block>
<Hairline />

View File

@ -1,6 +1,5 @@
// @flow
import { Divider } from '@material-ui/core'
import ClickAwayListener from '@material-ui/core/ClickAwayListener'
import { ClickAwayListener, Divider } from '@material-ui/core'
import Menu from '@material-ui/core/Menu'
import MenuItem from '@material-ui/core/MenuItem'
import { makeStyles } from '@material-ui/core/styles'
@ -25,6 +24,7 @@ const useStyles = makeStyles({
'&:hover': {
backgroundColor: '#F0EFEE',
},
outline: 'none',
},
increasedPopperZindex: {
zIndex: 2001,
@ -43,9 +43,7 @@ const EllipsisTransactionDetails = ({ address, knownAddress }: EllipsisTransacti
const dispatch = useDispatch()
const currentSafeAddress = useSelector(safeParamAddressFromStateSelector)
const handleClick = (event) => {
setAnchorEl(event.currentTarget)
}
const handleClick = (event) => setAnchorEl(event.currentTarget)
const closeMenuHandler = () => setAnchorEl(null)
@ -56,8 +54,8 @@ const EllipsisTransactionDetails = ({ address, knownAddress }: EllipsisTransacti
return (
<ClickAwayListener onClickAway={closeMenuHandler}>
<div className={classes.container} onClick={handleClick} onKeyDown={handleClick} role="menu" tabIndex={0}>
<MoreHorizIcon />
<div className={classes.container} role="menu" tabIndex={0}>
<MoreHorizIcon onClick={handleClick} onKeyDown={handleClick} />
<Menu anchorEl={anchorEl} id="simple-menu" keepMounted onClose={closeMenuHandler} open={Boolean(anchorEl)}>
<MenuItem disabled onClick={closeMenuHandler}>
Send Again

View File

@ -44,6 +44,7 @@ import type { OwnerRow } from '~/routes/safe/components/Settings/ManageOwners/da
import RemoveOwnerIcon from '~/routes/safe/components/Settings/assets/icons/bin.svg'
import RemoveOwnerIconDisabled from '~/routes/safe/components/Settings/assets/icons/disabled-bin.svg'
import { addressBookQueryParamsSelector, safesListSelector } from '~/routes/safe/store/selectors'
import { checksumAddress } from '~/utils/checksumAddress'
type Props = {
classes: Object,
@ -69,7 +70,8 @@ const AddressBookTable = ({ classes }: Props) => {
useEffect(() => {
if (entryAddressToEditOrCreateNew) {
const key = addressBook.findKey((entry) => entry.address === entryAddressToEditOrCreateNew)
const checksumEntryAdd = checksumAddress(entryAddressToEditOrCreateNew)
const key = addressBook.findKey((entry) => entry.address === checksumEntryAdd)
if (key >= 0) {
// Edit old entry
const value = addressBook.get(key)
@ -79,7 +81,7 @@ const AddressBookTable = ({ classes }: Props) => {
setSelectedEntry({
entry: {
name: '',
address: entryAddressToEditOrCreateNew,
address: checksumEntryAdd,
isNew: true,
},
})
@ -89,17 +91,25 @@ const AddressBookTable = ({ classes }: Props) => {
const newEntryModalHandler = (entry: AddressBookEntry) => {
setEditCreateEntryModalOpen(false)
dispatch(addAddressBookEntry(makeAddressBookEntry(entry)))
const checksumEntries = {
...entry,
address: checksumAddress(entry.address),
}
dispatch(addAddressBookEntry(makeAddressBookEntry(checksumEntries)))
}
const editEntryModalHandler = (entry: AddressBookEntry) => {
setSelectedEntry(null)
setEditCreateEntryModalOpen(false)
dispatch(updateAddressBookEntry(makeAddressBookEntry(entry)))
const checksumEntries = {
...entry,
address: checksumAddress(entry.address),
}
dispatch(updateAddressBookEntry(makeAddressBookEntry(checksumEntries)))
}
const deleteEntryModalHandler = () => {
const entryAddress = selectedEntry.entry.address
const entryAddress = checksumAddress(selectedEntry.entry.address)
setSelectedEntry(null)
setDeleteEntryModalOpen(false)
dispatch(removeAddressBookEntry(entryAddress))

View File

@ -13,6 +13,7 @@ import GnoForm from '~/components/forms/GnoForm'
import { required } from '~/components/forms/validator'
import Img from '~/components/layout/Img'
import appsIconSvg from '~/routes/safe/components/Transactions/TxsTable/TxType/assets/appsIcon.svg'
import { isValid as isURLValid } from '~/utils/url'
const FORM_ID = 'add-apps-form'
@ -51,9 +52,7 @@ type Props = {
}
const urlValidator = (value: string) => {
return /(?:^|[ \t])((https?:\/\/)?(?:localhost|[\w-]+(?:\.[\w-]+)+)(:\d+)?(\/\S*)?)/gm.test(value)
? undefined
: 'Please, provide a valid url'
return isURLValid(value) ? undefined : 'Please, provide a valid url'
}
const composeValidatorsApps = (...validators: Function[]): FieldValidator => (value: Field, values, meta) => {
@ -92,7 +91,15 @@ const ManageApps = ({ appList, onAppAdded, onAppToggle }: Props) => {
}
const uniqueAppValidator = (value) => {
const exists = appList.find((a) => a.url === value.trim())
const exists = appList.find((a) => {
try {
const currentUrl = new URL(a.url)
const newUrl = new URL(value)
return currentUrl.href === newUrl.href
} catch (error) {
return 'There was a problem trying to validate the URL existence.'
}
})
return exists ? 'This app is already registered.' : undefined
}

View File

@ -1,4 +1,5 @@
// @flow
import { BigNumber } from 'bignumber.js'
import React from 'react'
import styled from 'styled-components'
@ -8,6 +9,8 @@ import Heading from '~/components/layout/Heading'
import Img from '~/components/layout/Img'
import { getEthAsToken } from '~/logic/tokens/utils/tokenHelpers'
const humanReadableBalance = (balance, decimals) => BigNumber(balance).times(`1e-${decimals}`).toFixed()
const Wrapper = styled.div`
margin-bottom: 15px;
`
@ -28,13 +31,14 @@ const confirmTransactions = (
safeAddress: string,
safeName: string,
ethBalance: string,
nameApp: string,
iconApp: string,
txs: Array<any>,
openModal: () => void,
closeModal: () => void,
onConfirm: () => void,
) => {
const title = <ModalTitle iconUrl={iconApp} title="Compound" />
const title = <ModalTitle iconUrl={iconApp} title={nameApp} />
const body = (
<>
@ -49,7 +53,7 @@ const confirmTransactions = (
<Heading tag="h3">Value</Heading>
<div className="value-section">
<Img alt="Ether" height={40} src={getEthAsToken('0').logoUri} />
<Bold>{tx.value} ETH</Bold>
<Bold>{humanReadableBalance(tx.value, 18)} ETH</Bold>
</div>
</div>
<div className="section">

View File

@ -21,6 +21,7 @@ import {
safeParamAddressFromStateSelector,
} from '~/routes/safe/store/selectors'
import { loadFromStorage, saveToStorage } from '~/utils/storage'
import { isSameHref } from '~/utils/url'
const APPS_STORAGE_KEY = 'APPS_STORAGE_KEY'
const APPS_LEGAL_DISCLAIMER_STORAGE_KEY = 'APPS_LEGAL_DISCLAIMER_STORAGE_KEY'
@ -30,7 +31,6 @@ const StyledIframe = styled.iframe`
box-sizing: border-box;
width: 100%;
height: 100%;
display: ${(props) => (props.shouldDisplay ? 'block' : 'none')};
`
const Centered = styled.div`
display: flex;
@ -68,7 +68,8 @@ function Apps({ closeModal, closeSnackbar, enqueueSnackbar, openModal }: Props)
const getSelectedApp = () => appList.find((e) => e.id === selectedApp)
const sendMessageToIframe = (messageId, data) => {
iframeEl.contentWindow.postMessage({ messageId, data }, getSelectedApp().url)
const app = getSelectedApp()
iframeEl.contentWindow.postMessage({ messageId, data }, app.url)
}
const handleIframeMessage = async (data) => {
@ -89,6 +90,7 @@ function Apps({ closeModal, closeSnackbar, enqueueSnackbar, openModal }: Props)
safeAddress,
safeName,
ethBalance,
getSelectedApp().name,
getSelectedApp().iconUrl,
data.data,
openModal,
@ -172,16 +174,18 @@ function Apps({ closeModal, closeSnackbar, enqueueSnackbar, openModal }: Props)
)
}
const app = getSelectedApp()
return (
<>
{appIsLoading && <Loader />}
<StyledIframe
frameBorder="0"
id="iframeId"
id={`iframe-${app.name}`}
ref={iframeRef}
shouldDisplay={!appIsLoading}
src={getSelectedApp().url}
title={getSelectedApp().name}
src={app.url}
title={app.name}
/>
</>
)
@ -250,8 +254,9 @@ function Apps({ closeModal, closeSnackbar, enqueueSnackbar, openModal }: Props)
return
}
if (!getSelectedApp().url.includes(origin)) {
console.error(`ThirdPartyApp: A message from was received from an unknown origin ${origin}`)
const app = getSelectedApp()
if (!app.url.includes(origin)) {
console.error(`ThirdPartyApp: A message was received from an unknown origin ${origin}`)
return
}
@ -263,7 +268,7 @@ function Apps({ closeModal, closeSnackbar, enqueueSnackbar, openModal }: Props)
return () => {
window.removeEventListener('message', onIframeMessage)
}
})
}, [selectedApp])
// load legalDisclaimer
useEffect(() => {
@ -276,7 +281,7 @@ function Apps({ closeModal, closeSnackbar, enqueueSnackbar, openModal }: Props)
}
checkLegalDisclaimer()
})
}, [])
// Load apps list
useEffect(() => {
@ -333,7 +338,8 @@ function Apps({ closeModal, closeSnackbar, enqueueSnackbar, openModal }: Props)
})
}
if (!iframeEl) {
const app = getSelectedApp()
if (!iframeEl || !selectedApp || !isSameHref(iframeEl.src, app.url)) {
return
}
@ -342,7 +348,7 @@ function Apps({ closeModal, closeSnackbar, enqueueSnackbar, openModal }: Props)
return () => {
iframeEl.removeEventListener('load', onIframeLoaded)
}
}, [iframeEl])
}, [iframeEl, selectedApp])
if (loading) {
return <Loader />

View File

@ -3,7 +3,14 @@ import axios from 'axios'
import appsIconSvg from '~/routes/safe/components/Transactions/TxsTable/TxType/assets/appsIcon.svg'
const gnosisAppsUrl = process.env.REACT_APP_GNOSIS_APPS_URL
const removeLastTrailingSlash = (url: string) => {
if (url.substr(-1) === '/') {
return url.substr(0, url.length - 1)
}
return url
}
const gnosisAppsUrl = removeLastTrailingSlash(process.env.REACT_APP_GNOSIS_APPS_URL)
export const staticAppsList = [
{ url: `${gnosisAppsUrl}/compound`, disabled: false },
{ url: `${gnosisAppsUrl}/aave`, disabled: false },
@ -29,40 +36,42 @@ export const getAppInfoFromUrl = async (appUrl: string) => {
return res
}
let cleanedUpAppUrl = appUrl.trim()
if (cleanedUpAppUrl.substr(-1) === '/') {
cleanedUpAppUrl = cleanedUpAppUrl.substr(0, cleanedUpAppUrl.length - 1)
res.url = cleanedUpAppUrl
}
res.url = appUrl.trim()
let noTrailingSlashUrl = removeLastTrailingSlash(res.url)
try {
const appInfo = await axios.get(`${cleanedUpAppUrl}/manifest.json`)
const appInfo = await axios.get(`${noTrailingSlashUrl}/manifest.json`)
// verify imported app fulfil safe requirements
if (!appInfo || !appInfo.data || !appInfo.data.name || !appInfo.data.description) {
throw Error('The app does not fulfil the structure required.')
}
// the DB origin field has a limit of 100 characters
const originFieldSize = 100
const jsonDataLength = 20
const remainingSpace = originFieldSize - res.url.length - jsonDataLength
res = {
...res,
...appInfo.data,
id: JSON.stringify({ url: cleanedUpAppUrl, name: appInfo.data.name }),
id: JSON.stringify({ url: res.url, name: appInfo.data.name.substring(0, remainingSpace) }),
error: false,
}
if (appInfo.data.iconPath) {
try {
const iconInfo = await axios.get(`${cleanedUpAppUrl}/${appInfo.data.iconPath}`)
const iconInfo = await axios.get(`${noTrailingSlashUrl}/${appInfo.data.iconPath}`, { timeout: 1000 * 10 })
if (/image\/\w/gm.test(iconInfo.headers['content-type'])) {
res.iconUrl = `${cleanedUpAppUrl}/${appInfo.data.iconPath}`
res.iconUrl = `${noTrailingSlashUrl}/${appInfo.data.iconPath}`
}
} catch (error) {
console.error(`It was not possible to fetch icon from app ${cleanedUpAppUrl}`)
console.error(`It was not possible to fetch icon from app ${res.url}`)
}
}
return res
} catch (error) {
console.error(`It was not possible to fetch app from ${cleanedUpAppUrl}: ${error.message}`)
console.error(`It was not possible to fetch app from ${res.url}: ${error.message}`)
return res
}
}

View File

@ -19,8 +19,8 @@ import Button from '~/components/layout/Button'
import Row from '~/components/layout/Row'
import {
currencyRateSelector,
currencyValuesListSelector,
currentCurrencySelector,
safeFiatBalancesListSelector,
} from '~/logic/currencyValues/store/selectors'
import { BALANCE_ROW_TEST_ID } from '~/routes/safe/components/Balances'
import AssetTableCell from '~/routes/safe/components/Balances/AssetTableCell'
@ -46,16 +46,16 @@ const Coins = (props: Props) => {
const classes = useStyles()
const columns = generateColumns()
const autoColumns = columns.filter((c) => !c.custom)
const currencySelected = useSelector(currentCurrencySelector)
const selectedCurrency = useSelector(currentCurrencySelector)
const currencyRate = useSelector(currencyRateSelector)
const activeTokens = useSelector(extendedSafeTokensSelector)
const currencyValues = useSelector(currencyValuesListSelector)
const currencyValues = useSelector(safeFiatBalancesListSelector)
const granted = useSelector(grantedSelector)
const [filteredData, setFilteredData] = React.useState(List())
React.useMemo(() => {
setFilteredData(getBalanceData(activeTokens, currencySelected, currencyValues, currencyRate))
}, [currencySelected, currencyRate, activeTokens.hashCode(), currencyValues.hashCode()])
setFilteredData(getBalanceData(activeTokens, selectedCurrency, currencyValues, currencyRate))
}, [selectedCurrency, currencyRate, activeTokens.hashCode(), currencyValues])
return (
<TableContainer>

View File

@ -9,20 +9,21 @@ import ArrowDown from '../assets/arrow-down.svg'
import { styles } from './style'
import QRIcon from '~/assets/icons/qrcode.svg'
import CopyBtn from '~/components/CopyBtn'
import EtherscanBtn from '~/components/EtherscanBtn'
import Identicon from '~/components/Identicon'
import ScanQRModal from '~/components/ScanQRModal'
import { ScanQRWrapper } from '~/components/ScanQRModal/ScanQRWrapper'
import WhenFieldChanges from '~/components/WhenFieldChanges'
import GnoForm from '~/components/forms/GnoForm'
import Block from '~/components/layout/Block'
import Button from '~/components/layout/Button'
import Col from '~/components/layout/Col'
import Hairline from '~/components/layout/Hairline'
import Img from '~/components/layout/Img'
import Paragraph from '~/components/layout/Paragraph'
import Row from '~/components/layout/Row'
import type { AddressBook } from '~/logic/addressBook/model/addressBook'
import { getAddressBook } from '~/logic/addressBook/store/selectors'
import { getNameFromAdbk } from '~/logic/addressBook/utils'
import type { NFTAssetsState, NFTTokensState } from '~/logic/collectibles/store/reducer/collectibles'
import { nftTokensSelector, safeActiveSelectorMap } from '~/logic/collectibles/store/selectors'
import type { NFTToken } from '~/routes/safe/components/Balances/Collectibles/types'
@ -60,7 +61,7 @@ const SendCollectible = ({ initialValues, onClose, onNext, recipientAddress, sel
const { address: safeAddress, ethBalance, name: safeName } = useSelector(safeSelector)
const nftAssets: NFTAssetsState = useSelector(safeActiveSelectorMap)
const nftTokens: NFTTokensState = useSelector(nftTokensSelector)
const [qrModalOpen, setQrModalOpen] = useState<boolean>(false)
const addressBook: AddressBook = useSelector(getAddressBook)
const [selectedEntry, setSelectedEntry] = useState<Object | null>({
address: recipientAddress || initialValues.recipientAddress,
name: '',
@ -85,14 +86,6 @@ const SendCollectible = ({ initialValues, onClose, onNext, recipientAddress, sel
onNext(values)
}
const openQrModal = () => {
setQrModalOpen(true)
}
const closeQrModal = () => {
setQrModalOpen(false)
}
return (
<>
<Row align="center" className={classes.heading} grow>
@ -112,14 +105,18 @@ const SendCollectible = ({ initialValues, onClose, onNext, recipientAddress, sel
const { assetAddress } = formState.values
const selectedNFTTokens = nftTokens.filter((nftToken) => nftToken.assetAddress === assetAddress)
const handleScan = (value) => {
const handleScan = (value, closeQrModal) => {
let scannedAddress = value
if (scannedAddress.startsWith('ethereum:')) {
scannedAddress = scannedAddress.replace('ethereum:', '')
}
const scannedName = addressBook ? getNameFromAdbk(addressBook, scannedAddress) : ''
mutators.setRecipient(scannedAddress)
setSelectedEntry({
name: scannedName,
address: scannedAddress,
})
closeQrModal()
}
@ -200,16 +197,7 @@ const SendCollectible = ({ initialValues, onClose, onNext, recipientAddress, sel
/>
</Col>
<Col center="xs" className={classes} middle="xs" xs={1}>
<Img
alt="Scan QR"
className={classes.qrCodeBtn}
height={20}
onClick={() => {
openQrModal()
}}
role="button"
src={QRIcon}
/>
<ScanQRWrapper handleScan={handleScan} />
</Col>
</Row>
</>
@ -256,7 +244,6 @@ const SendCollectible = ({ initialValues, onClose, onNext, recipientAddress, sel
Review
</Button>
</Row>
{qrModalOpen && <ScanQRModal isOpen={qrModalOpen} onClose={closeQrModal} onScan={handleScan} />}
</>
)
}}

View File

@ -10,11 +10,10 @@ import ArrowDown from '../assets/arrow-down.svg'
import { styles } from './style'
import QRIcon from '~/assets/icons/qrcode.svg'
import CopyBtn from '~/components/CopyBtn'
import EtherscanBtn from '~/components/EtherscanBtn'
import Identicon from '~/components/Identicon'
import ScanQRModal from '~/components/ScanQRModal'
import { ScanQRWrapper } from '~/components/ScanQRModal/ScanQRWrapper'
import Field from '~/components/forms/Field'
import GnoForm from '~/components/forms/GnoForm'
import TextField from '~/components/forms/TextField'
@ -25,9 +24,11 @@ import Button from '~/components/layout/Button'
import ButtonLink from '~/components/layout/ButtonLink'
import Col from '~/components/layout/Col'
import Hairline from '~/components/layout/Hairline'
import Img from '~/components/layout/Img'
import Paragraph from '~/components/layout/Paragraph'
import Row from '~/components/layout/Row'
import type { AddressBook } from '~/logic/addressBook/model/addressBook'
import { getAddressBook } from '~/logic/addressBook/store/selectors'
import { getNameFromAdbk } from '~/logic/addressBook/utils'
import SafeInfo from '~/routes/safe/components/Balances/SendModal/SafeInfo'
import AddressBookInput from '~/routes/safe/components/Balances/SendModal/screens/AddressBookInput'
import { safeSelector } from '~/routes/safe/store/selectors'
@ -45,13 +46,13 @@ const useStyles = makeStyles(styles)
const SendCustomTx = ({ initialValues, onClose, onNext, recipientAddress }: Props) => {
const classes = useStyles()
const { address: safeAddress, ethBalance, name: safeName } = useSelector(safeSelector)
const [qrModalOpen, setQrModalOpen] = useState<boolean>(false)
const [selectedEntry, setSelectedEntry] = useState<Object | null>({
address: recipientAddress || initialValues.recipientAddress,
name: '',
})
const [pristine, setPristine] = useState<boolean>(true)
const [isValidAddress, setIsValidAddress] = useState<boolean>(true)
const addressBook: AddressBook = useSelector(getAddressBook)
React.useMemo(() => {
if (selectedEntry === null && pristine) {
@ -65,14 +66,6 @@ const SendCustomTx = ({ initialValues, onClose, onNext, recipientAddress }: Prop
}
}
const openQrModal = () => {
setQrModalOpen(true)
}
const closeQrModal = () => {
setQrModalOpen(false)
}
const formMutators = {
setMax: (args, state, utils) => {
utils.changeValue(state, 'value', () => ethBalance)
@ -103,14 +96,18 @@ const SendCustomTx = ({ initialValues, onClose, onNext, recipientAddress }: Prop
shouldDisableSubmitButton = !selectedEntry.address
}
const handleScan = (value) => {
const handleScan = (value, closeQrModal) => {
let scannedAddress = value
if (scannedAddress.startsWith('ethereum:')) {
scannedAddress = scannedAddress.replace('ethereum:', '')
}
const scannedName = addressBook ? getNameFromAdbk(addressBook, scannedAddress) : ''
mutators.setRecipient(scannedAddress)
setSelectedEntry({
name: scannedName,
address: scannedAddress,
})
closeQrModal()
}
@ -184,16 +181,7 @@ const SendCustomTx = ({ initialValues, onClose, onNext, recipientAddress }: Prop
/>
</Col>
<Col center="xs" className={classes} middle="xs" xs={1}>
<Img
alt="Scan QR"
className={classes.qrCodeBtn}
height={20}
onClick={() => {
openQrModal()
}}
role="button"
src={QRIcon}
/>
<ScanQRWrapper handleScan={handleScan} />
</Col>
</Row>
</>
@ -252,7 +240,6 @@ const SendCustomTx = ({ initialValues, onClose, onNext, recipientAddress }: Prop
Review
</Button>
</Row>
{qrModalOpen && <ScanQRModal isOpen={qrModalOpen} onClose={closeQrModal} onScan={handleScan} />}
</>
)
}}

View File

@ -11,11 +11,10 @@ import ArrowDown from '../assets/arrow-down.svg'
import { styles } from './style'
import QRIcon from '~/assets/icons/qrcode.svg'
import CopyBtn from '~/components/CopyBtn'
import EtherscanBtn from '~/components/EtherscanBtn'
import Identicon from '~/components/Identicon'
import ScanQRModal from '~/components/ScanQRModal'
import { ScanQRWrapper } from '~/components/ScanQRModal/ScanQRWrapper'
import Field from '~/components/forms/Field'
import GnoForm from '~/components/forms/GnoForm'
import TextField from '~/components/forms/TextField'
@ -25,9 +24,11 @@ import Button from '~/components/layout/Button'
import ButtonLink from '~/components/layout/ButtonLink'
import Col from '~/components/layout/Col'
import Hairline from '~/components/layout/Hairline'
import Img from '~/components/layout/Img'
import Paragraph from '~/components/layout/Paragraph'
import Row from '~/components/layout/Row'
import type { AddressBook } from '~/logic/addressBook/model/addressBook'
import { getAddressBook } from '~/logic/addressBook/store/selectors'
import { getNameFromAdbk } from '~/logic/addressBook/utils'
import { type Token } from '~/logic/tokens/store/model/token'
import SafeInfo from '~/routes/safe/components/Balances/SendModal/SafeInfo'
import AddressBookInput from '~/routes/safe/components/Balances/SendModal/screens/AddressBookInput'
@ -62,7 +63,7 @@ const SendFunds = ({ initialValues, onClose, onNext, recipientAddress, selectedT
const classes = useStyles()
const { address: safeAddress, ethBalance, name: safeName } = useSelector(safeSelector)
const tokens: Token = useSelector(extendedSafeTokensSelector)
const [qrModalOpen, setQrModalOpen] = useState<boolean>(false)
const addressBook: AddressBook = useSelector(getAddressBook)
const [selectedEntry, setSelectedEntry] = useState<Object | null>({
address: recipientAddress || initialValues.recipientAddress,
name: '',
@ -85,14 +86,6 @@ const SendFunds = ({ initialValues, onClose, onNext, recipientAddress, selectedT
onNext(submitValues)
}
const openQrModal = () => {
setQrModalOpen(true)
}
const closeQrModal = () => {
setQrModalOpen(false)
}
return (
<>
<Row align="center" className={classes.heading} grow>
@ -112,14 +105,18 @@ const SendFunds = ({ initialValues, onClose, onNext, recipientAddress, selectedT
const { token: tokenAddress } = formState.values
const selectedTokenRecord = tokens.find((token) => token.address === tokenAddress)
const handleScan = (value) => {
const handleScan = (value, closeQrModal) => {
let scannedAddress = value
if (scannedAddress.startsWith('ethereum:')) {
scannedAddress = scannedAddress.replace('ethereum:', '')
}
const scannedName = addressBook ? getNameFromAdbk(addressBook, scannedAddress) : ''
mutators.setRecipient(scannedAddress)
setSelectedEntry({
name: scannedName,
address: scannedAddress,
})
closeQrModal()
}
@ -198,16 +195,7 @@ const SendFunds = ({ initialValues, onClose, onNext, recipientAddress, selectedT
/>
</Col>
<Col center="xs" className={classes} middle="xs" xs={1}>
<Img
alt="Scan QR"
className={classes.qrCodeBtn}
height={20}
onClick={() => {
openQrModal()
}}
role="button"
src={QRIcon}
/>
<ScanQRWrapper handleScan={handleScan} />
</Col>
</Row>
</>
@ -276,7 +264,6 @@ const SendFunds = ({ initialValues, onClose, onNext, recipientAddress, selectedT
Review
</Button>
</Row>
{qrModalOpen && <ScanQRModal isOpen={qrModalOpen} onClose={closeQrModal} onScan={handleScan} />}
</>
)
}}

View File

@ -21,8 +21,8 @@ import Img from '~/components/layout/Img'
import Paragraph from '~/components/layout/Paragraph'
import Row from '~/components/layout/Row'
import { type Token, type TokenProps } from '~/logic/tokens/store/model/token'
import { getWeb3 } from '~/logic/wallets/getWeb3'
import TokenPlaceholder from '~/routes/safe/components/Balances/assets/token_placeholder.svg'
import { checksumAddress } from '~/utils/checksumAddress'
export const ADD_CUSTOM_TOKEN_ADDRESS_INPUT_TEST_ID = 'add-custom-token-address-input'
export const ADD_CUSTOM_TOKEN_SYMBOLS_INPUT_TEST_ID = 'add-custom-token-symbols-input'
@ -65,7 +65,7 @@ const AddCustomToken = (props: Props) => {
const [formValues, setFormValues] = useState(INITIAL_FORM_STATE)
const handleSubmit = (values) => {
const address = getWeb3().utils.toChecksumAddress(values.address)
const address = checksumAddress(values.address)
const token = {
address,
decimals: values.decimals,

View File

@ -13,18 +13,19 @@ import { useDispatch, useSelector } from 'react-redux'
import CheckIcon from './img/check.svg'
import fetchCurrencySelectedValue from '~/logic/currencyValues/store/actions/fetchCurrencySelectedValue'
import saveCurrencySelected from '~/logic/currencyValues/store/actions/saveCurrencySelected'
import { setSelectedCurrency } from '~/logic/currencyValues/store/actions/setSelectedCurrency'
import { AVAILABLE_CURRENCIES } from '~/logic/currencyValues/store/model/currencyValues'
import { currentCurrencySelector } from '~/logic/currencyValues/store/selectors'
import { useDropdownStyles } from '~/routes/safe/components/DropdownCurrency/style'
import { safeParamAddressFromStateSelector } from '~/routes/safe/store/selectors'
import { DropdownListTheme } from '~/theme/mui'
const DropdownCurrency = () => {
const currenciesList = Object.values(AVAILABLE_CURRENCIES)
const safeAddress = useSelector(safeParamAddressFromStateSelector)
const dispatch = useDispatch()
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null)
const currencyValueSelected = useSelector(currentCurrencySelector)
const selectedCurrency = useSelector(currentCurrencySelector)
const [searchParams, setSearchParams] = useState('')
const classes = useDropdownStyles()
@ -41,17 +42,16 @@ const DropdownCurrency = () => {
}
const onCurrentCurrencyChangedHandler = (newCurrencySelectedName: AVAILABLE_CURRENCIES) => {
dispatch(fetchCurrencySelectedValue(newCurrencySelectedName))
dispatch(saveCurrencySelected(newCurrencySelectedName))
dispatch(setSelectedCurrency(safeAddress, newCurrencySelectedName))
handleClose()
}
return !currencyValueSelected ? null : (
return !selectedCurrency ? null : (
<MuiThemeProvider theme={DropdownListTheme}>
<>
<button className={classes.button} onClick={handleClick} type="button">
<span className={classNames(classes.buttonInner, anchorEl && classes.openMenuButton)}>
{currencyValueSelected}
{selectedCurrency}
</span>
</button>
<Menu
@ -108,7 +108,7 @@ const DropdownCurrency = () => {
/>
</ListItemIcon>
<ListItemText primary={currencyName} />
{currencyName === currencyValueSelected ? (
{currencyName === selectedCurrency ? (
<ListItemIcon className={classes.iconRight}>
<img alt="checked" src={CheckIcon} />
</ListItemIcon>

View File

@ -100,7 +100,7 @@ const TabsComponent = (props: Props) => {
label={labelTransactions}
value={`${match.url}/transactions`}
/>
{!process.env.REACT_APP_APPS_DISABLED && (
{process.env.REACT_APP_APPS_DISABLED !== 'true' && (
<Tab
classes={{
selected: classes.tabWrapperSelected,

View File

@ -91,7 +91,7 @@ const Layout = (props: Props) => {
<Switch>
<Route exact path={`${match.path}/balances/:assetType?`} render={() => wrapInSuspense(<Balances />, null)} />
<Route exact path={`${match.path}/transactions`} render={() => wrapInSuspense(<TxsTable />, null)} />
{!process.env.REACT_APP_APPS_DISABLED && (
{process.env.REACT_APP_APPS_DISABLED !== 'true' && (
<Route
exact
path={`${match.path}/apps`}

View File

@ -17,6 +17,7 @@ import addSafeOwner from '~/routes/safe/store/actions/addSafeOwner'
import createTransaction from '~/routes/safe/store/actions/createTransaction'
import { type Owner } from '~/routes/safe/store/models/owner'
import { safeOwnersSelector, safeParamAddressFromStateSelector } from '~/routes/safe/store/selectors'
import { checksumAddress } from '~/utils/checksumAddress'
const styles = () => ({
biggerModalWindow: {
@ -91,7 +92,7 @@ const AddOwner = ({ classes, closeSnackbar, enqueueSnackbar, isOpen, onClose }:
setValues((stateValues) => ({
...stateValues,
ownerName: newValues.ownerName,
ownerAddress: newValues.ownerAddress,
ownerAddress: checksumAddress(newValues.ownerAddress),
}))
setActiveScreen('selectThreshold')
}

View File

@ -7,6 +7,7 @@ import { useSelector } from 'react-redux'
import { styles } from './style'
import { ScanQRWrapper } from '~/components/ScanQRModal/ScanQRWrapper'
import AddressInput from '~/components/forms/AddressInput'
import Field from '~/components/forms/Field'
import GnoForm from '~/components/forms/GnoForm'
@ -59,6 +60,16 @@ const OwnerForm = ({ classes, onClose, onSubmit }: Props) => {
{(...args) => {
const mutators = args[3]
const handleScan = (value, closeQrModal) => {
let scannedAddress = value
if (scannedAddress.startsWith('ethereum:')) {
scannedAddress = scannedAddress.replace('ethereum:', '')
}
mutators.setOwnerAddress(scannedAddress)
closeQrModal()
}
return (
<>
<Block className={classes.formContainer}>
@ -91,6 +102,9 @@ const OwnerForm = ({ classes, onClose, onSubmit }: Props) => {
validators={[ownerDoesntExist]}
/>
</Col>
<Col center="xs" className={classes} middle="xs" xs={1}>
<ScanQRWrapper handleScan={handleScan} />
</Col>
</Row>
</Block>
<Hairline />

View File

@ -14,6 +14,7 @@ import { TX_NOTIFICATION_TYPES } from '~/logic/safe/transactions'
import createTransaction from '~/routes/safe/store/actions/createTransaction'
import replaceSafeOwner from '~/routes/safe/store/actions/replaceSafeOwner'
import { safeParamAddressFromStateSelector, safeThresholdSelector } from '~/routes/safe/store/selectors'
import { checksumAddress } from '~/utils/checksumAddress'
const styles = () => ({
biggerModalWindow: {
@ -96,8 +97,10 @@ const ReplaceOwner = ({ classes, closeSnackbar, enqueueSnackbar, isOpen, onClose
const onClickBack = () => setActiveScreen('checkOwner')
const ownerSubmitted = (newValues: Object) => {
values.ownerName = newValues.ownerName
values.ownerAddress = newValues.ownerAddress
const { ownerAddress, ownerName } = newValues
const checksumAddr = checksumAddress(ownerAddress)
values.ownerName = ownerName
values.ownerAddress = checksumAddr
setValues(values)
setActiveScreen('reviewReplaceOwner')
}

View File

@ -11,6 +11,7 @@ import { styles } from './style'
import CopyBtn from '~/components/CopyBtn'
import EtherscanBtn from '~/components/EtherscanBtn'
import Identicon from '~/components/Identicon'
import { ScanQRWrapper } from '~/components/ScanQRModal/ScanQRWrapper'
import AddressInput from '~/components/forms/AddressInput'
import Field from '~/components/forms/Field'
import GnoForm from '~/components/forms/GnoForm'
@ -65,6 +66,17 @@ const OwnerForm = ({ classes, onClose, onSubmit, ownerAddress, ownerName }: Prop
{(...args) => {
const mutators = args[3]
const handleScan = (value, closeQrModal) => {
let scannedAddress = value
if (scannedAddress.startsWith('ethereum:')) {
scannedAddress = scannedAddress.replace('ethereum:', '')
}
mutators.setOwnerAddress(scannedAddress)
closeQrModal()
}
return (
<>
<Block className={classes.formContainer}>
@ -126,6 +138,9 @@ const OwnerForm = ({ classes, onClose, onSubmit, ownerAddress, ownerName }: Prop
validators={[ownerDoesntExist]}
/>
</Col>
<Col center="xs" className={classes} middle="xs" xs={1}>
<ScanQRWrapper handleScan={handleScan} />
</Col>
</Row>
</Block>
<Hairline />

View File

@ -1,11 +1,12 @@
// @flow
import { makeStyles } from '@material-ui/core/styles'
import React from 'react'
import { useSelector } from 'react-redux'
import EtherscanLink from '~/components/EtherscanLink'
import Block from '~/components/layout/Block'
import Bold from '~/components/layout/Bold'
import { getNameFromAddressBook } from '~/logic/addressBook/utils'
import { getNameFromAddressBook } from '~/logic/addressBook/store/selectors'
import OwnerAddressTableCell from '~/routes/safe/components/Settings/ManageOwners/OwnerAddressTableCell'
import { getIncomingTxAmount } from '~/routes/safe/components/Transactions/TxsTable/columns'
import type { IncomingTransaction } from '~/routes/safe/store/models/incomingTransaction'
@ -46,7 +47,7 @@ const TransferDescription = ({ from, txFromName, value = '' }: TransferDescProps
const IncomingTxDescription = ({ tx }: Props) => {
const classes = useStyles()
const txFromName = getNameFromAddressBook(tx.from)
const txFromName = useSelector((state) => getNameFromAddressBook(state, tx.from))
return (
<Block className={classes.txDataContainer}>
<TransferDescription from={tx.from} txFromName={txFromName} value={getIncomingTxAmount(tx, false)} />

View File

@ -2,6 +2,7 @@
import { withStyles } from '@material-ui/core/styles'
import cn from 'classnames'
import React from 'react'
import { useSelector } from 'react-redux'
import CancelSmallFilledCircle from './assets/cancel-small-filled.svg'
import ConfirmSmallFilledCircle from './assets/confirm-small-filled.svg'
@ -16,8 +17,7 @@ import Block from '~/components/layout/Block'
import Button from '~/components/layout/Button'
import Img from '~/components/layout/Img'
import Paragraph from '~/components/layout/Paragraph'
import { getNameFromAddressBook } from '~/logic/addressBook/utils'
import { type Owner } from '~/routes/safe/store/models/owner'
import { getNameFromAddressBook } from '~/logic/addressBook/store/selectors'
export const CONFIRM_TX_BTN_TEST_ID = 'confirm-btn'
export const EXECUTE_TX_BTN_TEST_ID = 'execute-btn'
@ -32,7 +32,7 @@ type OwnerProps = {
onTxReject?: Function,
onTxConfirm: Function,
onTxExecute: Function,
owner: Owner,
owner: string,
showRejectBtn: boolean,
showExecuteRejectBtn: boolean,
showConfirmBtn: boolean,
@ -57,8 +57,7 @@ const OwnerComponent = ({
thresholdReached,
userAddress,
}: OwnerProps) => {
const nameInAdbk = getNameFromAddressBook(owner.address)
const ownerName = nameInAdbk || owner.name
const nameInAdbk = useSelector((state) => getNameFromAddressBook(state, owner))
const [imgCircle, setImgCircle] = React.useState(ConfirmSmallGreyCircle)
React.useMemo(() => {
@ -77,15 +76,15 @@ const OwnerComponent = ({
<div className={classes.circleState}>
<Img alt="" src={imgCircle} />
</div>
<Identicon address={owner.address} className={classes.icon} diameter={32} />
<Identicon address={owner} className={classes.icon} diameter={32} />
<Block>
<Paragraph className={classes.name} noMargin>
{ownerName}
{nameInAdbk}
</Paragraph>
<EtherscanLink className={classes.address} cut={4} type="address" value={owner.address} />
<EtherscanLink className={classes.address} cut={4} type="address" value={owner} />
</Block>
<Block className={classes.spacer} />
{owner.address === userAddress && (
{owner === userAddress && (
<Block>
{isCancelTx ? (
<>
@ -140,7 +139,7 @@ const OwnerComponent = ({
)}
</Block>
)}
{owner.address === executor && <Block className={classes.executor}>Executor</Block>}
{owner === executor && <Block className={classes.executor}>Executor</Block>}
</Block>
)
}

View File

@ -48,7 +48,7 @@ const OwnersList = ({
confirmed
executor={executor}
isCancelTx={isCancelTx}
key={owner.address}
key={owner}
onTxExecute={onTxExecute}
onTxReject={onTxReject}
owner={owner}
@ -64,7 +64,7 @@ const OwnersList = ({
classes={classes}
executor={executor}
isCancelTx={isCancelTx}
key={owner.address}
key={owner}
onTxConfirm={onTxConfirm}
onTxExecute={onTxExecute}
onTxReject={onTxReject}

View File

@ -39,7 +39,7 @@ function getOwnersConfirmations(tx, userAddress) {
let currentUserAlreadyConfirmed = false
tx.confirmations.forEach((conf) => {
if (conf.owner.address === userAddress) {
if (conf.owner === userAddress) {
currentUserAlreadyConfirmed = true
}
@ -52,18 +52,20 @@ function getOwnersConfirmations(tx, userAddress) {
}
function getPendingOwnersConfirmations(owners, tx, userAddress) {
const ownersUnconfirmed = owners.filter(
(owner) => tx.confirmations.findIndex((conf) => conf.owner.address === owner.address) === -1,
)
const ownersNotConfirmed = []
let currentUserNotConfirmed = true
let userIsUnconfirmedOwner = false
ownersUnconfirmed.some((owner) => {
userIsUnconfirmedOwner = owner.address === userAddress
return userIsUnconfirmedOwner
owners.forEach((owner) => {
const confirmationsEntry = tx.confirmations.find((conf) => conf.owner === owner.address)
if (!confirmationsEntry) {
ownersNotConfirmed.push(owner.address)
}
if (confirmationsEntry && confirmationsEntry.owner === userAddress) {
currentUserNotConfirmed = false
}
})
return [ownersUnconfirmed, userIsUnconfirmedOwner]
return [ownersNotConfirmed, currentUserNotConfirmed]
}
const OwnersColumn = ({

View File

@ -1,6 +1,7 @@
// @flow
import { withStyles } from '@material-ui/core/styles'
import React, { useState } from 'react'
import { useSelector } from 'react-redux'
import { getTxData } from './utils'
@ -9,7 +10,7 @@ import Block from '~/components/layout/Block'
import Bold from '~/components/layout/Bold'
import LinkWithRef from '~/components/layout/Link'
import Paragraph from '~/components/layout/Paragraph'
import { getNameFromAddressBook } from '~/logic/addressBook/utils'
import { getNameFromAddressBook } from '~/logic/addressBook/store/selectors'
import { SAFE_METHODS_NAMES } from '~/logic/contracts/methodIds'
import { shortVersionOf } from '~/logic/wallets/ethAddresses'
import OwnerAddressTableCell from '~/routes/safe/components/Settings/ManageOwners/OwnerAddressTableCell'
@ -68,7 +69,7 @@ type CustomDescProps = {
}
const TransferDescription = ({ amount = '', recipient }: TransferDescProps) => {
const recipientName = getNameFromAddressBook(recipient)
const recipientName = useSelector((state) => getNameFromAddressBook(state, recipient))
return (
<Block data-testid={TRANSACTIONS_DESC_SEND_TEST_ID}>
<Bold>Send {amount} to:</Bold>
@ -82,7 +83,7 @@ const TransferDescription = ({ amount = '', recipient }: TransferDescProps) => {
}
const RemovedOwner = ({ removedOwner }: { removedOwner: string }) => {
const ownerChangedName = getNameFromAddressBook(removedOwner)
const ownerChangedName = useSelector((state) => getNameFromAddressBook(state, removedOwner))
return (
<Block data-testid={TRANSACTIONS_DESC_REMOVE_OWNER_TEST_ID}>
@ -97,7 +98,7 @@ const RemovedOwner = ({ removedOwner }: { removedOwner: string }) => {
}
const AddedOwner = ({ addedOwner }: { addedOwner: string }) => {
const ownerChangedName = getNameFromAddressBook(addedOwner)
const ownerChangedName = useSelector((state) => getNameFromAddressBook(state, addedOwner))
return (
<Block data-testid={TRANSACTIONS_DESC_ADD_OWNER_TEST_ID}>
@ -161,7 +162,7 @@ const SettingsDescription = ({ action, addedOwner, newThreshold, removedOwner }:
const CustomDescription = ({ amount = 0, classes, data, recipient }: CustomDescProps) => {
const [showTxData, setShowTxData] = useState(false)
const recipientName = getNameFromAddressBook(recipient)
const recipientName = useSelector((state) => getNameFromAddressBook(state, recipient))
return (
<>
<Block data-testid={TRANSACTIONS_DESC_CUSTOM_VALUE_TEST_ID}>

View File

@ -2,6 +2,7 @@
import { useMemo } from 'react'
import { batch, useDispatch, useSelector } from 'react-redux'
import fetchCollectibles from '~/logic/collectibles/store/actions/fetchCollectibles'
import { fetchCurrencyValues } from '~/logic/currencyValues/store/actions/fetchCurrencyValues'
import activateAssetsByBalance from '~/logic/tokens/store/actions/activateAssetsByBalance'
import fetchSafeTokens from '~/logic/tokens/store/actions/fetchSafeTokens'
@ -24,7 +25,11 @@ export const useFetchTokens = () => {
}
if (COLLECTIBLES_LOCATION_REGEX.test(history.location.pathname)) {
batch(() => {
dispatch(fetchCollectibles()).then(() => {
dispatch(activateAssetsByBalance(address))
})
})
}
}, [history.location.pathname])
}

View File

@ -34,7 +34,7 @@ const getTxStatus = (tx: Transaction, userAddress: string, safe: Safe): Transact
} else if (!tx.confirmations.size) {
txStatus = 'pending'
} else {
const userConfirmed = tx.confirmations.filter((conf) => conf.owner.address === userAddress).size === 1
const userConfirmed = tx.confirmations.filter((conf) => conf.owner === userAddress).size === 1
const userIsSafeOwner = safe.owners.filter((owner) => owner.address === userAddress).size === 1
txStatus = !userConfirmed && userIsSafeOwner ? 'awaiting_your_confirmation' : 'awaiting_confirmations'
}

View File

@ -7,7 +7,7 @@ import generateBatchRequests from '~/logic/contracts/generateBatchRequests'
import { getLocalSafe, getSafeName } from '~/logic/safe/utils'
import { enabledFeatures, safeNeedsUpdate } from '~/logic/safe/utils/safeVersion'
import { sameAddress } from '~/logic/wallets/ethAddresses'
import { getBalanceInEtherOf, getWeb3 } from '~/logic/wallets/getWeb3'
import { getBalanceInEtherOf } from '~/logic/wallets/getWeb3'
import addSafe from '~/routes/safe/store/actions/addSafe'
import addSafeOwner from '~/routes/safe/store/actions/addSafeOwner'
import removeSafeOwner from '~/routes/safe/store/actions/removeSafeOwner'
@ -15,29 +15,31 @@ import updateSafe from '~/routes/safe/store/actions/updateSafe'
import { makeOwner } from '~/routes/safe/store/models/owner'
import type { SafeProps } from '~/routes/safe/store/models/safe'
import { type GlobalState } from '~/store'
import { checksumAddress } from '~/utils/checksumAddress'
const buildOwnersFrom = (
safeOwners: string[],
localSafe: SafeProps | {}, // eslint-disable-next-line
) =>
safeOwners.map((ownerAddress: string) => {
const convertedAdd = checksumAddress(ownerAddress)
if (!localSafe) {
return makeOwner({ name: 'UNKNOWN', address: ownerAddress })
return makeOwner({ name: 'UNKNOWN', address: convertedAdd })
}
const storedOwner = localSafe.owners.find(({ address }) => sameAddress(address, ownerAddress))
const storedOwner = localSafe.owners.find(({ address }) => sameAddress(address, convertedAdd))
if (!storedOwner) {
return makeOwner({ name: 'UNKNOWN', address: ownerAddress })
return makeOwner({ name: 'UNKNOWN', address: convertedAdd })
}
return makeOwner({
name: storedOwner.name || 'UNKNOWN',
address: ownerAddress,
address: convertedAdd,
})
})
export const buildSafe = async (safeAdd: string, safeName: string, latestMasterContractVersion: string) => {
const safeAddress = getWeb3().utils.toChecksumAddress(safeAdd)
const safeAddress = checksumAddress(safeAdd)
const safeParams = ['getThreshold', 'nonce', 'VERSION', 'getOwners']
const [[thresholdStr, nonceStr, currentVersion, remoteOwners], localSafe, ethBalance] = await Promise.all([
@ -72,7 +74,7 @@ export const buildSafe = async (safeAdd: string, safeName: string, latestMasterC
}
export const checkAndUpdateSafe = (safeAdd: string) => async (dispatch: ReduxDispatch<*>) => {
const safeAddress = getWeb3().utils.toChecksumAddress(safeAdd)
const safeAddress = checksumAddress(safeAdd)
// Check if the owner's safe did change and update them
const safeParams = ['getThreshold', 'nonce', 'getOwners']
const [[remoteThreshold, remoteNonce, remoteOwners], localSafe] = await Promise.all([
@ -125,7 +127,7 @@ export const checkAndUpdateSafe = (safeAdd: string) => async (dispatch: ReduxDis
// eslint-disable-next-line consistent-return
export default (safeAdd: string) => async (dispatch: ReduxDispatch<GlobalState>, getState: () => GlobalState) => {
try {
const safeAddress = getWeb3().utils.toChecksumAddress(safeAdd)
const safeAddress = checksumAddress(safeAdd)
const safeName = (await getSafeName(safeAddress)) || 'LOADED SAFE'
const latestMasterContractVersion = getState().safes.get('latestMasterContractVersion')
const safeProps: SafeProps = await buildSafe(safeAddress, safeName, latestMasterContractVersion)

View File

@ -13,7 +13,6 @@ import generateBatchRequests from '~/logic/contracts/generateBatchRequests'
import { decodeParamsFromSafeMethod } from '~/logic/contracts/methodIds'
import { buildIncomingTxServiceUrl } from '~/logic/safe/transactions/incomingTxHistory'
import { type TxServiceType, buildTxServiceUrl } from '~/logic/safe/transactions/txHistory'
import { getLocalSafe } from '~/logic/safe/utils'
import { TOKEN_REDUCER_ID } from '~/logic/tokens/store/reducer/tokens'
import { ALTERNATIVE_TOKEN_ABI } from '~/logic/tokens/utils/alternativeAbi'
import {
@ -28,7 +27,6 @@ import { getWeb3 } from '~/logic/wallets/getWeb3'
import { addCancellationTransactions } from '~/routes/safe/store/actions/addCancellationTransactions'
import { makeConfirmation } from '~/routes/safe/store/models/confirmation'
import { type IncomingTransaction, makeIncomingTransaction } from '~/routes/safe/store/models/incomingTransaction'
import { makeOwner } from '~/routes/safe/store/models/owner'
import type { TransactionProps } from '~/routes/safe/store/models/transaction'
import { type Transaction, makeTransaction } from '~/routes/safe/store/models/transaction'
import { type GlobalState } from '~/store'
@ -83,27 +81,15 @@ export const buildTransactionFrom = async (
txTokenName,
txTokenSymbol,
): Promise<Transaction> => {
const localSafe = await getLocalSafe(safeAddress)
const confirmations = List(
tx.confirmations.map((conf: ConfirmationServiceModel) => {
let ownerName = 'UNKNOWN'
if (localSafe && localSafe.owners) {
const storedOwner = localSafe.owners.find((owner) => sameAddress(conf.owner, owner.address))
if (storedOwner) {
ownerName = storedOwner.name
}
}
return makeConfirmation({
owner: makeOwner({ address: conf.owner, name: ownerName }),
tx.confirmations.map((conf: ConfirmationServiceModel) =>
makeConfirmation({
owner: conf.owner,
type: ((conf.confirmationType.toLowerCase(): any): TxServiceType),
hash: conf.transactionHash,
signature: conf.signature,
})
}),
),
)
const modifySettingsTx = sameAddress(tx.to, safeAddress) && Number(tx.value) === 0 && !!tx.data
const cancellationTx = sameAddress(tx.to, safeAddress) && Number(tx.value) === 0 && !tx.data

View File

@ -4,14 +4,13 @@ import type { Dispatch as ReduxDispatch } from 'redux'
import setDefaultSafe from './setDefaultSafe'
import { getDefaultSafe } from '~/logic/safe/utils'
import { getWeb3 } from '~/logic/wallets/getWeb3'
import { type GlobalState } from '~/store/index'
import { checksumAddress } from '~/utils/checksumAddress'
const loadDefaultSafe = () => async (dispatch: ReduxDispatch<GlobalState>) => {
try {
const defaultSafe: string = await getDefaultSafe()
const checksumed =
defaultSafe && defaultSafe.length > 0 ? getWeb3().utils.toChecksumAddress(defaultSafe) : defaultSafe
const checksumed = defaultSafe && defaultSafe.length > 0 ? checksumAddress(defaultSafe) : defaultSafe
dispatch(setDefaultSafe(checksumed))
} catch (err) {
// eslint-disable-next-line

View File

@ -3,17 +3,16 @@ import { Record } from 'immutable'
import type { RecordFactory, RecordOf } from 'immutable'
import { type TxServiceType } from '~/logic/safe/transactions/txHistory'
import { type Owner, makeOwner } from '~/routes/safe/store/models/owner'
export type ConfirmationProps = {
owner: Owner,
owner: string,
type: TxServiceType,
hash: string,
signature?: string,
}
export const makeConfirmation: RecordFactory<ConfirmationProps> = Record({
owner: makeOwner(),
owner: '',
type: 'initialised',
hash: '',
signature: null,

View File

@ -2,7 +2,6 @@
import { Map, Set } from 'immutable'
import { type ActionType, handleActions } from 'redux-actions'
import { getWeb3 } from '~/logic/wallets/getWeb3'
import { ACTIVATE_TOKEN_FOR_ALL_SAFES } from '~/routes/safe/store/actions/activateTokenForAllSafes'
import { ADD_SAFE, buildOwnersFrom } from '~/routes/safe/store/actions/addSafe'
import { ADD_SAFE_OWNER } from '~/routes/safe/store/actions/addSafeOwner'
@ -15,6 +14,7 @@ import { SET_LATEST_MASTER_CONTRACT_VERSION } from '~/routes/safe/store/actions/
import { UPDATE_SAFE } from '~/routes/safe/store/actions/updateSafe'
import { makeOwner } from '~/routes/safe/store/models/owner'
import SafeRecord, { type SafeProps } from '~/routes/safe/store/models/safe'
import { checksumAddress } from '~/utils/checksumAddress'
export const SAFE_REDUCER_ID = 'safes'
@ -22,7 +22,7 @@ export type SafeReducerState = Map<string, *>
export const buildSafe = (storedSafe: SafeProps) => {
const names = storedSafe.owners.map((owner) => owner.name)
const addresses = storedSafe.owners.map((owner) => getWeb3().utils.toChecksumAddress(owner.address))
const addresses = storedSafe.owners.map((owner) => checksumAddress(owner.address))
const owners = buildOwnersFrom(Array.from(names), Array.from(addresses))
const activeTokens = Set(storedSafe.activeTokens)
const activeAssets = Set(storedSafe.activeAssets)

View File

@ -5,7 +5,6 @@ import { type OutputSelector, createSelector } from 'reselect'
import { getWeb3 } from '~/logic/wallets/getWeb3'
import { SAFELIST_ADDRESS, SAFE_PARAM_ADDRESS } from '~/routes/routes'
import { type Confirmation } from '~/routes/safe/store/models/confirmation'
import type { IncomingTransaction } from '~/routes/safe/store/models/incomingTransaction'
import { type Safe } from '~/routes/safe/store/models/safe'
import { type Transaction } from '~/routes/safe/store/models/transaction'
@ -20,13 +19,14 @@ import {
import { SAFE_REDUCER_ID } from '~/routes/safe/store/reducer/safe'
import { TRANSACTIONS_REDUCER_ID, type State as TransactionsState } from '~/routes/safe/store/reducer/transactions'
import { type GlobalState } from '~/store/index'
import { checksumAddress } from '~/utils/checksumAddress'
export type RouterProps = {
match: Match,
}
type TransactionProps = {
transaction: Transaction,
export type SafeProps = {
safeAddress: string,
}
const safesStateSelector = (state: GlobalState): Map<string, *> => state[SAFE_REDUCER_ID]
@ -64,8 +64,6 @@ const cancellationTransactionsSelector = (state: GlobalState): CancelTransaction
const incomingTransactionsSelector = (state: GlobalState): IncomingTransactionsState =>
state[INCOMING_TRANSACTIONS_REDUCER_ID]
const oneTransactionSelector = (state: GlobalState, props: TransactionProps) => props.transaction
export const safeParamAddressFromStateSelector = (state: GlobalState): string | null => {
const match = matchPath(state.router.location.pathname, { path: `${SAFELIST_ADDRESS}/:safeAddress` })
@ -79,7 +77,7 @@ export const safeParamAddressFromStateSelector = (state: GlobalState): string |
export const safeParamAddressSelector = (state: GlobalState, props: RouterProps) => {
const urlAdd = props.match.params[SAFE_PARAM_ADDRESS]
return urlAdd ? getWeb3().utils.toChecksumAddress(urlAdd) : ''
return urlAdd ? checksumAddress(urlAdd) : ''
}
type TxSelectorType = OutputSelector<GlobalState, RouterProps, List<Transaction>>
@ -144,22 +142,6 @@ export const safeIncomingTransactionsSelector: IncomingTxSelectorType = createSe
},
)
export const confirmationsTransactionSelector: OutputSelector<GlobalState, TransactionProps, number> = createSelector(
oneTransactionSelector,
(tx: Transaction) => {
if (!tx) {
return 0
}
const confirmations: List<Confirmation> = tx.get('confirmations')
if (!confirmations) {
return 0
}
return confirmations.filter((confirmation: Confirmation) => confirmation.get('type') === 'confirmation').count()
},
)
export type SafeSelectorProps = Safe | typeof undefined
export const safeSelector: OutputSelector<GlobalState, RouterProps, SafeSelectorProps> = createSelector(
@ -169,7 +151,7 @@ export const safeSelector: OutputSelector<GlobalState, RouterProps, SafeSelector
if (!address) {
return undefined
}
const checksumed = getWeb3().utils.toChecksumAddress(address)
const checksumed = checksumAddress(address)
const safe = safes.get(checksumed)
return safe

View File

@ -15,6 +15,7 @@ import {
nftTokensReducer,
} from '~/logic/collectibles/store/reducer/collectibles'
import cookies, { COOKIES_REDUCER_ID } from '~/logic/cookies/store/reducer/cookies'
import currencyValuesStorageMiddleware from '~/logic/currencyValues/store/middleware'
import currencyValues, { CURRENCY_VALUES_KEY } from '~/logic/currencyValues/store/reducer/currencyValues'
import currentSession, {
CURRENT_SESSION_REDUCER_ID,
@ -55,6 +56,7 @@ const finalCreateStore = composeEnhancers(
providerWatcher,
notificationsMiddleware,
addressBookMiddleware,
currencyValuesStorageMiddleware,
),
)

View File

@ -0,0 +1,7 @@
// @flow
import { getWeb3 } from '~/logic/wallets/getWeb3'
export const checksumAddress = (address: string) => {
if (!address) return null
return getWeb3().utils.toChecksumAddress(address)
}

20
src/utils/url.js Normal file
View File

@ -0,0 +1,20 @@
// @flow
export const isValid = (url: string, protocolsAllowed = ['https:', 'http:']): boolean => {
try {
const urlInfo = new URL(url)
return protocolsAllowed.includes(urlInfo.protocol)
} catch (error) {
return false
}
}
export const isSameHref = (url1: string, url2: string) => {
try {
const a = new URL(url1)
const b = new URL(url2)
return a.href === b.href
} catch (error) {
return false
}
}

2959
yarn.lock

File diff suppressed because it is too large Load Diff