commit
8459af859f
|
@ -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 }}
|
||||
|
@ -28,20 +29,18 @@ jobs:
|
|||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
|
||||
- name: Patch node gyp on windows to support Visual Studio 2019
|
||||
if: startsWith(matrix.os, 'windows')
|
||||
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
|
||||
|
||||
|
|
|
@ -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 : '/';
|
||||
|
|
66
package.json
66
package.json
|
@ -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/safe-react-components": "https://github.com/gnosis/safe-react-components.git#a057248",
|
||||
"@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",
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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' }))`
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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} />}
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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'
|
||||
},
|
||||
)
|
||||
|
|
|
@ -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 []
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
||||
batch(() => {
|
||||
dispatch(setCurrencySelected(currencyValueSelected))
|
||||
dispatch(fetchCurrencySelectedValue(currencyValueSelected))
|
||||
// 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(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
|
||||
|
|
|
@ -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
|
|
@ -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,
|
||||
}),
|
||||
)
|
||||
|
|
|
@ -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 }),
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
}),
|
||||
)
|
|
@ -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
|
|
@ -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>
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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),
|
||||
)
|
||||
|
|
|
@ -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)) || {}
|
||||
}
|
|
@ -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
|
||||
}, {})
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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}`
|
||||
|
|
|
@ -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}`
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -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,64 +82,77 @@ export const safeFieldsValidation = async (values: Object) => {
|
|||
return errors
|
||||
}
|
||||
|
||||
const Details = ({ classes, errors, form }: Props) => (
|
||||
<>
|
||||
<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.
|
||||
<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.
|
||||
</Paragraph>
|
||||
</Block>
|
||||
<Block className={classes.root}>
|
||||
<Field
|
||||
component={TextField}
|
||||
name={FIELD_LOAD_NAME}
|
||||
placeholder="Name of the Safe"
|
||||
text="Safe name"
|
||||
type="text"
|
||||
validate={required}
|
||||
/>
|
||||
</Block>
|
||||
<Block className={classes.root} margin="lg">
|
||||
<AddressInput
|
||||
component={TextField}
|
||||
fieldMutator={(val) => {
|
||||
form.mutators.setValue(FIELD_LOAD_ADDRESS, val)
|
||||
}}
|
||||
inputAdornment={
|
||||
noErrorsOn(FIELD_LOAD_ADDRESS, errors) && {
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<CheckCircle className={classes.check} />
|
||||
</InputAdornment>
|
||||
),
|
||||
}
|
||||
}
|
||||
name={FIELD_LOAD_ADDRESS}
|
||||
placeholder="Safe Address*"
|
||||
text="Safe Address"
|
||||
type="text"
|
||||
/>
|
||||
</Block>
|
||||
<Block margin="sm">
|
||||
<Paragraph className={classes.links} color="primary" noMargin size="md">
|
||||
By continuing you consent with the{' '}
|
||||
<a href="https://safe.gnosis.io/terms" rel="noopener noreferrer" target="_blank">
|
||||
terms of use
|
||||
</a>{' '}
|
||||
and{' '}
|
||||
<a href="https://safe.gnosis.io/privacy" rel="noopener noreferrer" target="_blank">
|
||||
privacy policy
|
||||
</a>
|
||||
. Most importantly, you confirm that your funds are held securely in the Gnosis Safe, a smart contract on the
|
||||
Ethereum blockchain. These funds cannot be accessed by Gnosis at any point.
|
||||
</Paragraph>
|
||||
</Block>
|
||||
</>
|
||||
)
|
||||
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.
|
||||
<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.
|
||||
</Paragraph>
|
||||
</Block>
|
||||
<Block className={classes.root}>
|
||||
<Col xs={11}>
|
||||
<Field
|
||||
component={TextField}
|
||||
name={FIELD_LOAD_NAME}
|
||||
placeholder="Name of the Safe"
|
||||
text="Safe name"
|
||||
type="text"
|
||||
validate={required}
|
||||
/>
|
||||
</Col>
|
||||
</Block>
|
||||
<Block className={classes.root} margin="lg">
|
||||
<Col xs={11}>
|
||||
<AddressInput
|
||||
component={TextField}
|
||||
fieldMutator={(val) => {
|
||||
form.mutators.setValue(FIELD_LOAD_ADDRESS, val)
|
||||
}}
|
||||
inputAdornment={
|
||||
noErrorsOn(FIELD_LOAD_ADDRESS, errors) && {
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<CheckCircle className={classes.check} />
|
||||
</InputAdornment>
|
||||
),
|
||||
}
|
||||
}
|
||||
name={FIELD_LOAD_ADDRESS}
|
||||
placeholder="Safe Address*"
|
||||
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">
|
||||
By continuing you consent with the{' '}
|
||||
<a href="https://safe.gnosis.io/terms" rel="noopener noreferrer" target="_blank">
|
||||
terms of use
|
||||
</a>{' '}
|
||||
and{' '}
|
||||
<a href="https://safe.gnosis.io/privacy" rel="noopener noreferrer" target="_blank">
|
||||
privacy policy
|
||||
</a>
|
||||
. Most importantly, you confirm that your funds are held securely in the Gnosis Safe, a smart contract on the
|
||||
Ethereum blockchain. These funds cannot be accessed by Gnosis at any point.
|
||||
</Paragraph>
|
||||
</Block>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const DetailsForm = withStyles(styles)(Details)
|
||||
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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,34 +83,53 @@ 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">
|
||||
<Field
|
||||
className={classes.addressInput}
|
||||
component={TextField}
|
||||
defaultValue={entryToEdit ? entryToEdit.entry.name : undefined}
|
||||
name="name"
|
||||
placeholder={entryToEdit ? 'Entry name' : 'New entry'}
|
||||
testId={CREATE_ENTRY_INPUT_NAME_ID}
|
||||
text={entryToEdit ? 'Entry*' : 'New entry*'}
|
||||
type="text"
|
||||
validate={composeValidators(required, minMaxLength(1, 50))}
|
||||
/>
|
||||
<Col xs={11}>
|
||||
<Field
|
||||
className={classes.addressInput}
|
||||
component={TextField}
|
||||
defaultValue={entryToEdit ? entryToEdit.entry.name : undefined}
|
||||
name="name"
|
||||
placeholder={entryToEdit ? 'Entry name' : 'New entry'}
|
||||
testId={CREATE_ENTRY_INPUT_NAME_ID}
|
||||
text={entryToEdit ? 'Entry*' : 'New entry*'}
|
||||
type="text"
|
||||
validate={composeValidators(required, minMaxLength(1, 50))}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row margin="md">
|
||||
<AddressInput
|
||||
className={classes.addressInput}
|
||||
defaultValue={entryToEdit ? entryToEdit.entry.address : undefined}
|
||||
disabled={!!entryToEdit}
|
||||
fieldMutator={mutators.setOwnerAddress}
|
||||
name="address"
|
||||
placeholder="Owner address*"
|
||||
testId={CREATE_ENTRY_INPUT_ADDRESS_ID}
|
||||
text="Owner address*"
|
||||
validators={entryToEdit ? undefined : [entryDoesntExist]}
|
||||
/>
|
||||
<Col xs={11}>
|
||||
<AddressInput
|
||||
className={classes.addressInput}
|
||||
defaultValue={entryToEdit ? entryToEdit.entry.address : undefined}
|
||||
disabled={!!entryToEdit}
|
||||
fieldMutator={mutators.setOwnerAddress}
|
||||
name="address"
|
||||
placeholder="Owner address*"
|
||||
testId={CREATE_ENTRY_INPUT_ADDRESS_ID}
|
||||
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 />
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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} />}
|
||||
</>
|
||||
)
|
||||
}}
|
||||
|
|
|
@ -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} />}
|
||||
</>
|
||||
)
|
||||
}}
|
||||
|
|
|
@ -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} />}
|
||||
</>
|
||||
)
|
||||
}}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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`}
|
||||
|
|
|
@ -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')
|
||||
}
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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')
|
||||
}
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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)} />
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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 = ({
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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)) {
|
||||
dispatch(activateAssetsByBalance(address))
|
||||
batch(() => {
|
||||
dispatch(fetchCollectibles()).then(() => {
|
||||
dispatch(activateAssetsByBalance(address))
|
||||
})
|
||||
})
|
||||
}
|
||||
}, [history.location.pathname])
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue