Merge pull request #1493 from gnosis/release/v2.13.0

Release v2.13.0
This commit is contained in:
Daniel Sanchez 2020-10-19 14:45:26 +02:00 committed by GitHub
commit 319ad7554e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
161 changed files with 3410 additions and 1911 deletions

View File

@ -1,13 +1,8 @@
# You can leave this empty for rinkeby or use "mainnet" # You can leave this empty for rinkeby or use "mainnet"
REACT_APP_NETWORK= REACT_APP_NETWORK=
# For Rinkeby network
REACT_APP_GOOGLE_ANALYTICS_ID_RINKEBY=
# For Mainnet network (no needed on dev mode)
REACT_APP_GOOGLE_ANALYTICS_ID_MAINNET=
# For all environments # For all environments
REACT_APP_GOOGLE_ANALYTICS=
REACT_APP_INFURA_TOKEN= REACT_APP_INFURA_TOKEN=
REACT_APP_IPFS_GATEWAY=https://ipfs.io/ipfs REACT_APP_IPFS_GATEWAY=https://ipfs.io/ipfs
PUBLIC_URL=/app/ PUBLIC_URL=/app/

View File

@ -1,4 +1,4 @@
if: (branch = development) OR (branch = master) OR (type = pull_request) OR (tag IS present) if: (branch = development) OR (branch = master) OR (release/v2.13.0) OR (type = pull_request) OR (tag IS present)
sudo: required sudo: required
dist: bionic dist: bionic
language: node_js language: node_js
@ -10,12 +10,19 @@ matrix:
include: include:
- env: - env:
- REACT_APP_NETWORK='mainnet' - REACT_APP_NETWORK='mainnet'
- REACT_APP_GOOGLE_ANALYTICS=${REACT_APP_GOOGLE_ANALYTICS_ID_MAINNET}
- STAGING_BUCKET_NAME=${STAGING_MAINNET_BUCKET_NAME} - STAGING_BUCKET_NAME=${STAGING_MAINNET_BUCKET_NAME}
- REACT_APP_GNOSIS_APPS_URL=${REACT_APP_GNOSIS_APPS_URL_PROD} - REACT_APP_GNOSIS_APPS_URL=${REACT_APP_GNOSIS_APPS_URL_PROD}
if: (branch = master AND NOT type = pull_request) OR tag IS present if: (branch = master AND NOT type = pull_request) OR tag IS present
- env: - env:
- REACT_APP_NETWORK='rinkeby' - REACT_APP_NETWORK='rinkeby'
- REACT_APP_GOOGLE_ANALYTICS=${REACT_APP_GOOGLE_ANALYTICS_ID_RINKEBY}
- REACT_APP_GNOSIS_APPS_URL=${REACT_APP_GNOSIS_APPS_URL_STAGING} - REACT_APP_GNOSIS_APPS_URL=${REACT_APP_GNOSIS_APPS_URL_STAGING}
- env:
- REACT_APP_NETWORK='xdai'
- REACT_APP_GOOGLE_ANALYTICS=${REACT_APP_GOOGLE_ANALYTICS_ID_XDAI}
- STAGING_BUCKET_NAME=${STAGING_XDAI_BUCKET_NAME}
if: ((branch = master OR branch = release/v2.13.0) AND NOT type = pull_request) OR tag IS present
cache: cache:
yarn: true yarn: true
before_script: before_script:
@ -47,31 +54,44 @@ deploy:
secret_access_key: $AWS_SECRET_ACCESS_KEY secret_access_key: $AWS_SECRET_ACCESS_KEY
skip_cleanup: true skip_cleanup: true
local_dir: build local_dir: build
upload-dir: app upload_dir: app
region: $AWS_DEFAULT_REGION region: $AWS_DEFAULT_REGION
on: on:
branch: development branch: development
# Staging environment # Staging environment
- provider: s3 - provider: s3
bucket: $STAGING_BUCKET_NAME bucket: $STAGING_BUCKET_NAME
access_key_id: $AWS_ACCESS_KEY_ID access_key_id: $AWS_ACCESS_KEY_ID
secret_access_key: $AWS_SECRET_ACCESS_KEY secret_access_key: $AWS_SECRET_ACCESS_KEY
skip_cleanup: true skip_cleanup: true
local_dir: build local_dir: build
upload-dir: current/app upload_dir: current/app
region: $AWS_DEFAULT_REGION region: $AWS_DEFAULT_REGION
on: on:
branch: master branch: master
# Prepare production deployment # xDai testing on staging
- provider: s3
bucket: $STAGING_BUCKET_NAME
access_key_id: $AWS_ACCESS_KEY_ID
secret_access_key: $AWS_SECRET_ACCESS_KEY
skip_cleanup: true
local_dir: build
upload_dir: current/app
region: $AWS_DEFAULT_REGION
on:
branch: release/v2.13.0
condition: $REACT_APP_NETWORK = xdai
# Prepare production deployment
- provider: s3 - provider: s3
bucket: $STAGING_BUCKET_NAME bucket: $STAGING_BUCKET_NAME
secret_access_key: $AWS_SECRET_ACCESS_KEY secret_access_key: $AWS_SECRET_ACCESS_KEY
access_key_id: $AWS_ACCESS_KEY_ID access_key_id: $AWS_ACCESS_KEY_ID
skip_cleanup: true skip_cleanup: true
local_dir: build local_dir: build
upload-dir: releases/$TRAVIS_TAG upload_dir: releases/$TRAVIS_TAG
region: $AWS_DEFAULT_REGION region: $AWS_DEFAULT_REGION
on: on:
tags: true tags: true

View File

@ -5,7 +5,7 @@ module.exports = function override(config) {
config.plugins = [] config.plugins = []
} }
config.plugins.push( config.plugins.push(
new webpack.ContextReplacementPlugin(/truffle-(contract|interface-adapter)/, (data) => { new webpack.ContextReplacementPlugin(/@truffle\/(contract|interface-adapter)/, (data) => {
delete data.dependencies[0].critical delete data.dependencies[0].critical
return data return data
}), }),

View File

@ -1,6 +1,6 @@
{ {
"name": "safe-react", "name": "safe-react",
"version": "2.12.3", "version": "2.13.0",
"description": "Allowing crypto users manage funds in a safer way", "description": "Allowing crypto users manage funds in a safer way",
"website": "https://github.com/gnosis/safe-react#readme", "website": "https://github.com/gnosis/safe-react#readme",
"bugs": { "bugs": {
@ -61,8 +61,7 @@
"src/**/*.{js,jsx,ts,tsx}", "src/**/*.{js,jsx,ts,tsx}",
"!src/**/*.{.test.*}", "!src/**/*.{.test.*}",
"!src/**/test/**/*", "!src/**/test/**/*",
"!src/**/assets/**", "!src/**/assets/**"
"!src/config/**/*"
] ]
}, },
"productName": "Safe Multisig", "productName": "Safe Multisig",
@ -164,36 +163,38 @@
] ]
}, },
"dependencies": { "dependencies": {
"@gnosis.pm/safe-apps-sdk": "0.4.0", "@gnosis.pm/safe-apps-sdk": "0.4.2",
"@gnosis.pm/safe-contracts": "1.1.1-dev.2", "@gnosis.pm/safe-contracts": "1.1.1-dev.2",
"@gnosis.pm/safe-react-components": "https://github.com/gnosis/safe-react-components.git#1bf397f", "@gnosis.pm/safe-react-components": "https://github.com/gnosis/safe-react-components.git#70e57bdd1e0fd5dfdf5768076577c1e000b5fe28",
"@gnosis.pm/util-contracts": "2.0.6", "@gnosis.pm/util-contracts": "2.0.6",
"@ledgerhq/hw-transport-node-hid": "5.22.0", "@ledgerhq/hw-transport-node-hid": "5.26.0",
"@material-ui/core": "4.11.0", "@material-ui/core": "4.11.0",
"@material-ui/icons": "4.9.1", "@material-ui/icons": "4.9.1",
"@material-ui/lab": "4.0.0-alpha.56", "@material-ui/lab": "4.0.0-alpha.56",
"@openzeppelin/contracts": "3.1.0", "@openzeppelin/contracts": "3.1.0",
"@truffle/contract": "4.2.25",
"async-sema": "^3.1.0", "async-sema": "^3.1.0",
"axios": "0.20.0", "axios": "0.20.0",
"bignumber.js": "9.0.0", "bignumber.js": "9.0.1",
"bnc-onboard": "1.13.1", "bnc-onboard": "1.13.2",
"classnames": "^2.2.6", "classnames": "^2.2.6",
"concurrently": "^5.3.0", "concurrently": "^5.3.0",
"connected-react-router": "6.8.0", "connected-react-router": "6.8.0",
"coveralls": "^3.1.0", "coveralls": "^3.1.0",
"currency-flags": "2.1.2", "currency-flags": "2.1.2",
"date-fns": "2.15.0", "date-fns": "2.16.1",
"detect-port": "^1.3.0",
"electron-is-dev": "^1.2.0", "electron-is-dev": "^1.2.0",
"electron-log": "4.2.4", "electron-log": "4.2.4",
"electron-settings": "^4.0.2", "electron-settings": "^4.0.2",
"electron-updater": "4.3.4", "electron-updater": "4.3.5",
"eth-sig-util": "^2.5.3", "eth-sig-util": "^2.5.3",
"ethereum-blockies-base64": "^1.0.2", "ethereum-blockies-base64": "^1.0.2",
"ethereumjs-abi": "0.6.8", "ethereumjs-abi": "0.6.8",
"exponential-backoff": "^3.1.0", "exponential-backoff": "^3.1.0",
"express": "^4.17.1", "express": "^4.17.1",
"final-form": "^4.20.1", "final-form": "^4.20.1",
"final-form-calculate": "^1.3.1", "final-form-calculate": "^1.3.2",
"history": "4.10.1", "history": "4.10.1",
"immortal-db": "^1.1.0", "immortal-db": "^1.1.0",
"immutable": "^4.0.0-rc.12", "immutable": "^4.0.0-rc.12",
@ -202,16 +203,15 @@
"lodash.memoize": "^4.1.2", "lodash.memoize": "^4.1.2",
"material-ui-search-bar": "^1.0.0", "material-ui-search-bar": "^1.0.0",
"notistack": "https://github.com/gnosis/notistack.git#v0.9.4", "notistack": "https://github.com/gnosis/notistack.git#v0.9.4",
"open": "^7.2.0",
"polished": "3.6.7", "polished": "3.6.7",
"qrcode.react": "1.0.0", "qrcode.react": "1.0.0",
"query-string": "6.13.1", "query-string": "6.13.5",
"react": "16.13.1", "react": "16.13.1",
"react-dom": "16.13.1", "react-dom": "16.13.1",
"react-final-form": "^6.5.1", "react-final-form": "^6.5.1",
"react-final-form-listeners": "^1.0.2", "react-final-form-listeners": "^1.0.2",
"react-ga": "3.1.2", "react-ga": "3.1.2",
"react-hot-loader": "4.12.21", "react-hot-loader": "4.13.0",
"react-qr-reader": "^2.2.1", "react-qr-reader": "^2.2.1",
"react-redux": "7.2.1", "react-redux": "7.2.1",
"react-router-dom": "5.2.0", "react-router-dom": "5.2.0",
@ -224,7 +224,6 @@
"reselect": "^4.0.0", "reselect": "^4.0.0",
"semver": "7.3.2", "semver": "7.3.2",
"styled-components": "^5.2.0", "styled-components": "^5.2.0",
"truffle-contract": "4.0.31",
"web3": "1.2.9", "web3": "1.2.9",
"web3-core": "^1.2.11", "web3-core": "^1.2.11",
"web3-eth-contract": "^1.2.11", "web3-eth-contract": "^1.2.11",
@ -242,12 +241,12 @@
"@types/history": "4.6.2", "@types/history": "4.6.2",
"@types/jest": "^26.0.14", "@types/jest": "^26.0.14",
"@types/lodash.memoize": "^4.1.6", "@types/lodash.memoize": "^4.1.6",
"@types/node": "14.11.2", "@types/node": "^14.11.8",
"@types/react": "^16.9.49", "@types/react": "^16.9.52",
"@types/react-dom": "^16.9.6", "@types/react-dom": "^16.9.6",
"@types/react-redux": "^7.1.9", "@types/react-redux": "^7.1.9",
"@types/react-router-dom": "^5.1.5", "@types/react-router-dom": "^5.1.6",
"@types/styled-components": "^5.1.3", "@types/styled-components": "^5.1.4",
"@typescript-eslint/eslint-plugin": "3.9.1", "@typescript-eslint/eslint-plugin": "3.9.1",
"@typescript-eslint/parser": "3.9.1", "@typescript-eslint/parser": "3.9.1",
"autoprefixer": "9.8.6", "autoprefixer": "9.8.6",
@ -258,15 +257,15 @@
"electron-builder": "22.8.1", "electron-builder": "22.8.1",
"electron-notarize": "1.0.0", "electron-notarize": "1.0.0",
"eslint": "6.8.0", "eslint": "6.8.0",
"eslint-config-prettier": "6.11.0", "eslint-config-prettier": "6.12.0",
"eslint-plugin-import": "2.22.0", "eslint-plugin-import": "2.22.1",
"eslint-plugin-jsx-a11y": "^6.3.1", "eslint-plugin-jsx-a11y": "^6.3.1",
"eslint-plugin-prettier": "^3.1.4", "eslint-plugin-prettier": "^3.1.4",
"eslint-plugin-react": "^7.20.6", "eslint-plugin-react": "^7.21.4",
"eslint-plugin-sort-destructure-keys": "1.3.5", "eslint-plugin-sort-destructure-keys": "1.3.5",
"ethereumjs-abi": "0.6.8", "ethereumjs-abi": "0.6.8",
"husky": "^4.2.5", "husky": "^4.3.0",
"lint-staged": "10.4.0", "lint-staged": "^10.4.0",
"node-sass": "^4.14.1", "node-sass": "^4.14.1",
"prettier": "2.1.2", "prettier": "2.1.2",
"react-app-rewired": "^2.1.6", "react-app-rewired": "^2.1.6",

View File

@ -1,84 +1,88 @@
const electron = require("electron"); const electron = require('electron')
const express = require('express'); const express = require('express')
const open = require('open'); const log = require('electron-log')
const log = require('electron-log'); const fs = require('fs')
const fs = require('fs'); const Menu = electron.Menu
const Menu = electron.Menu; const https = require('https')
const https = require('https'); const detect = require('detect-port')
const autoUpdater = require('./auto-updater'); const autoUpdater = require('./auto-updater')
const app = electron.app; const { app, session, BrowserWindow, shell } = electron
const session = electron.session;
const BrowserWindow = electron.BrowserWindow;
const path = require("path"); const path = require('path')
const isDev = require("electron-is-dev"); const isDev = require('electron-is-dev')
const options = { const options = {
key: fs.readFileSync(path.join(__dirname, './ssl/server.key')), key: fs.readFileSync(path.join(__dirname, './ssl/server.key')),
cert: fs.readFileSync(path.join(__dirname, './ssl/server.crt')), cert: fs.readFileSync(path.join(__dirname, './ssl/server.crt')),
ca: fs.readFileSync(path.join(__dirname, './ssl/rootCA.crt')) ca: fs.readFileSync(path.join(__dirname, './ssl/rootCA.crt')),
};
const PORT = 5000;
const createServer = () => {
const app = express();
const staticRoute = path.join(__dirname, '../build');
app.use(express.static(staticRoute));
https.createServer(options, app).listen(PORT);
} }
const DEFAULT_PORT = 5000
let mainWindow; const createServer = async () => {
const app = express()
const staticRoute = path.join(__dirname, '../build')
app.use(express.static(staticRoute))
let selectedPort = DEFAULT_PORT
try {
const _port = await detect(DEFAULT_PORT)
if (_port !== DEFAULT_PORT) selectedPort = _port
https.createServer(options, app).listen(selectedPort)
} catch (e) {
log.error(e)
} finally {
return selectedPort
}
}
function getOpenedWindow(url,options) { let mainWindow
let display = electron.screen.getPrimaryDisplay();
let width = display.bounds.width; function getOpenedWindow(url, options) {
let height = display.bounds.height; let display = electron.screen.getPrimaryDisplay()
let width = display.bounds.width
let height = display.bounds.height
// filter all requests to trezor-bridge and change origin to make it work // filter all requests to trezor-bridge and change origin to make it work
const filter = { const filter = {
urls: ['http://127.0.0.1:21325/*'] urls: ['http://127.0.0.1:21325/*'],
};
options.webPreferences.affinity = 'main-window';
if(url.includes('trezor')){
session.defaultSession.webRequest.onBeforeSendHeaders(filter, (details, callback) => {
details.requestHeaders['Origin'] = 'https://connect.trezor.io';
callback({cancel: false, requestHeaders: details.requestHeaders});
});
} }
if(url.includes('wallet.portis') || url.includes('trezor') || url.includes('app.tor.us')){ options.webPreferences.affinity = 'main-window'
if (url.includes('trezor')) {
session.defaultSession.webRequest.onBeforeSendHeaders(filter, (details, callback) => {
details.requestHeaders['Origin'] = 'https://connect.trezor.io'
callback({ cancel: false, requestHeaders: details.requestHeaders })
})
}
if (url.includes('wallet.portis') || url.includes('trezor') || url.includes('app.tor.us')) {
const win = new BrowserWindow({ const win = new BrowserWindow({
width:350, width: 350,
height:700, height: 700,
x: width - 1300, x: width - 1300,
parent:mainWindow, parent: mainWindow,
y: height - (process.platform === 'win32' ? 750 : 200), y: height - (process.platform === 'win32' ? 750 : 200),
webContents: options.webContents, // use existing webContents if provided webContents: options.webContents, // use existing webContents if provided
fullscreen: false, fullscreen: false,
show: false, show: false,
}); })
win.webContents.on('new-window', function(event, url){ win.webContents.on('new-window', function (event, url) {
if(url.includes('trezor') && url.includes('bridge')) if (url.includes('trezor') && url.includes('bridge')) shell.openExternal(url)
open(url); })
}); win.once('ready-to-show', () => win.show())
win.once('ready-to-show', () => win.show());
if(!options.webPreferences){ if (!options.webPreferences) {
win.loadURL(url); win.loadURL(url)
} }
return win return win
} }
return null; return null
} }
function createWindow() { function createWindow(port = DEFAULT_PORT) {
mainWindow = new BrowserWindow({ mainWindow = new BrowserWindow({
show: false, show: false,
width: 1024, width: 1024,
@ -89,79 +93,77 @@ function createWindow() {
nativeWindowOpen: true, // need to be set in order to display modal nativeWindowOpen: true, // need to be set in order to display modal
}, },
icon: electron.nativeImage.createFromPath(path.join(__dirname, './build/safe.png')), icon: electron.nativeImage.createFromPath(path.join(__dirname, './build/safe.png')),
}); })
mainWindow.once('ready-to-show', () => { mainWindow.once('ready-to-show', () => {
mainWindow.show(); mainWindow.show()
}); })
mainWindow.loadURL( mainWindow.loadURL(isDev ? 'http://localhost:3000' : `https://localhost:${port}`)
isDev
? "http://localhost:3000"
: `https://localhost:${PORT}`
)
if (isDev) { if (isDev) {
// Open the DevTools. // Open the DevTools.
mainWindow.webContents.openDevTools(); mainWindow.webContents.openDevTools()
//BrowserWindow.addDevToolsExtension('<location to your react chrome extension>'); //BrowserWindow.addDevToolsExtension('<location to your react chrome extension>');
} }
mainWindow.setMenu(null); mainWindow.setMenu(null)
mainWindow.setMenuBarVisibility(false); mainWindow.setMenuBarVisibility(false)
mainWindow.webContents.on('new-window', function(event, url, frameName, disposition, options){ mainWindow.webContents.on('new-window', function (event, url, frameName, disposition, options) {
event.preventDefault(); event.preventDefault()
const win = getOpenedWindow(url,options); const win = getOpenedWindow(url, options)
if(win){ if (win) {
win.once('ready-to-show', () => win.show()); win.once('ready-to-show', () => win.show())
if(!options.webPreferences){ if (!options.webPreferences) {
win.loadURL(url); win.loadURL(url)
} }
event.newGuest = win event.newGuest = win
} else open(url); } else shell.openExternal(url)
}); })
mainWindow.webContents.on('did-finish-load', () => { mainWindow.webContents.on('did-finish-load', () => {
autoUpdater.init(mainWindow); autoUpdater.init(mainWindow)
}); })
mainWindow.webContents.on('crashed', (event) => { mainWindow.webContents.on('crashed', (event) => {
log.info(`App Crashed: ${event}`); log.info(`App Crashed: ${event}`)
mainWindow.reload(); mainWindow.reload()
}); })
mainWindow.on("closed", () => (mainWindow = null)); mainWindow.on('closed', () => (mainWindow = null))
} }
process.on('uncaughtException',function(error){ process.on('uncaughtException', function (error) {
log.error(error); log.error(error)
}); })
app.userAgentFallback = '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'; app.userAgentFallback =
'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'
// We have one non-context-aware module in node_modules/usb. This is used by @ledgerhq/hw-transport-node-hid // We have one non-context-aware module in node_modules/usb. This is used by @ledgerhq/hw-transport-node-hid
// This type of modules will be impossible to use after electron 10 // This type of modules will be impossible to use after electron 10
app.allowRendererProcessReuse = false; app.allowRendererProcessReuse = false
app.commandLine.appendSwitch('ignore-certificate-errors'); app.commandLine.appendSwitch('ignore-certificate-errors')
app.on("ready", () =>{ app.on('ready', async () => {
// Hide the menu // Hide the menu
Menu.setApplicationMenu(null); Menu.setApplicationMenu(null)
if(!isDev) createServer(); let usedPort = DEFAULT_PORT
createWindow(); if (!isDev) usedPort = await createServer()
}); createWindow(usedPort)
})
app.on("window-all-closed", () => { app.on('window-all-closed', () => {
if (process.platform !== "darwin") { if (process.platform !== 'darwin') {
app.quit(); app.quit()
} }
}); })
app.on("activate", () => { app.on('activate', () => {
if (mainWindow === null) { if (mainWindow === null) {
createWindow(); createWindow()
} }
}); })

View File

@ -1,6 +1,5 @@
import React from 'react' import React from 'react'
import styled from 'styled-components' import { getNetworkInfo } from 'src/config'
import CopyBtn from 'src/components/CopyBtn' import CopyBtn from 'src/components/CopyBtn'
import EtherscanBtn from 'src/components/EtherscanBtn' import EtherscanBtn from 'src/components/EtherscanBtn'
import Identicon from 'src/components/Identicon' import Identicon from 'src/components/Identicon'
@ -8,6 +7,7 @@ import Block from 'src/components/layout/Block'
import Bold from 'src/components/layout/Bold' import Bold from 'src/components/layout/Bold'
import Paragraph from 'src/components/layout/Paragraph' import Paragraph from 'src/components/layout/Paragraph'
import { border, xs } from 'src/theme/variables' import { border, xs } from 'src/theme/variables'
import styled from 'styled-components'
const Wrapper = styled.div` const Wrapper = styled.div`
display: flex; display: flex;
@ -42,6 +42,8 @@ interface Props {
ethBalance?: string ethBalance?: string
} }
const { nativeCoin } = getNetworkInfo()
const AddressInfo = ({ ethBalance, safeAddress, safeName }: Props): React.ReactElement => { const AddressInfo = ({ ethBalance, safeAddress, safeName }: Props): React.ReactElement => {
return ( return (
<Wrapper> <Wrapper>
@ -59,12 +61,12 @@ const AddressInfo = ({ ethBalance, safeAddress, safeName }: Props): React.ReactE
{safeAddress} {safeAddress}
</Paragraph> </Paragraph>
<CopyBtn content={safeAddress} /> <CopyBtn content={safeAddress} />
<EtherscanBtn type="address" value={safeAddress} /> <EtherscanBtn value={safeAddress} />
</div> </div>
{ethBalance && ( {ethBalance && (
<StyledBlock> <StyledBlock>
<Paragraph noMargin> <Paragraph noMargin>
Balance: <Bold data-testid="current-eth-balance">{`${ethBalance} ETH`}</Bold> Balance: <Bold data-testid="current-eth-balance">{`${ethBalance} ${nativeCoin.symbol}`}</Bold>
</Paragraph> </Paragraph>
</StyledBlock> </StyledBlock>
)} )}

View File

@ -2,7 +2,7 @@ import IconButton from '@material-ui/core/IconButton'
import { createStyles, makeStyles } from '@material-ui/core/styles' import { createStyles, makeStyles } from '@material-ui/core/styles'
import Close from '@material-ui/icons/Close' import Close from '@material-ui/icons/Close'
import QRCode from 'qrcode.react' import QRCode from 'qrcode.react'
import * as React from 'react' import React, { ReactElement } from 'react'
import CopyBtn from 'src/components/CopyBtn' import CopyBtn from 'src/components/CopyBtn'
import EtherscanBtn from 'src/components/EtherscanBtn' import EtherscanBtn from 'src/components/EtherscanBtn'
@ -13,9 +13,11 @@ import Col from 'src/components/layout/Col'
import Hairline from 'src/components/layout/Hairline' import Hairline from 'src/components/layout/Hairline'
import Paragraph from 'src/components/layout/Paragraph' import Paragraph from 'src/components/layout/Paragraph'
import Row from 'src/components/layout/Row' import Row from 'src/components/layout/Row'
import { lg, md, screenSm, secondaryText, sm } from 'src/theme/variables' import { border, fontColor, lg, md, screenSm, secondaryText, sm } from 'src/theme/variables'
import { copyToClipboard } from 'src/utils/clipboard' import { copyToClipboard } from 'src/utils/clipboard'
import { getNetworkInfo } from 'src/config'
const networkInfo = getNetworkInfo()
const useStyles = makeStyles( const useStyles = makeStyles(
createStyles({ createStyles({
heading: { heading: {
@ -35,6 +37,12 @@ const useStyles = makeStyles(
borderRadius: '6px', borderRadius: '6px',
border: `1px solid ${secondaryText}`, border: `1px solid ${secondaryText}`,
}, },
networkInfo: {
backgroundColor: `${networkInfo?.backgroundColor ?? border}`,
color: `${networkInfo?.textColor ?? fontColor}`,
padding: md,
marginBottom: 0,
},
annotation: { annotation: {
margin: lg, margin: lg,
marginBottom: 0, marginBottom: 0,
@ -79,7 +87,7 @@ type Props = {
safeName: string safeName: string
} }
const ReceiveModal = ({ onClose, safeAddress, safeName }: Props) => { const ReceiveModal = ({ onClose, safeAddress, safeName }: Props): ReactElement => {
const classes = useStyles() const classes = useStyles()
return ( return (
@ -93,9 +101,12 @@ const ReceiveModal = ({ onClose, safeAddress, safeName }: Props) => {
</IconButton> </IconButton>
</Row> </Row>
<Hairline /> <Hairline />
<Paragraph className={classes.networkInfo} noMargin size="lg" weight="bolder">
{networkInfo.label} Network only send {networkInfo.label} assets to this Safe.
</Paragraph>
<Paragraph className={classes.annotation} noMargin size="lg"> <Paragraph className={classes.annotation} noMargin size="lg">
This is the address of your Safe. Deposit funds by scanning the QR code or copying the address below. Only send This is the address of your Safe. Deposit funds by scanning the QR code or copying the address below. Only send{' '}
ETH and ERC-20 tokens to this address! {networkInfo.nativeCoin.name} and ERC-20 tokens to this address!
</Paragraph> </Paragraph>
<Col layout="column" middle="xs"> <Col layout="column" middle="xs">
<Paragraph className={classes.safeName} noMargin size="lg" weight="bold"> <Paragraph className={classes.safeName} noMargin size="lg" weight="bold">
@ -115,7 +126,7 @@ const ReceiveModal = ({ onClose, safeAddress, safeName }: Props) => {
{safeAddress} {safeAddress}
</Paragraph> </Paragraph>
<CopyBtn content={safeAddress} /> <CopyBtn content={safeAddress} />
<EtherscanBtn type="address" value={safeAddress} /> <EtherscanBtn value={safeAddress} />
</Block> </Block>
</Col> </Col>
<Hairline /> <Hairline />

View File

@ -16,8 +16,8 @@ import CookiesBanner from 'src/components/CookiesBanner'
import Notifier from 'src/components/Notifier' import Notifier from 'src/components/Notifier'
import Backdrop from 'src/components/layout/Backdrop' import Backdrop from 'src/components/layout/Backdrop'
import Img from 'src/components/layout/Img' import Img from 'src/components/layout/Img'
import { getNetwork } from 'src/config' import { getNetworkId } from 'src/config'
import { ETHEREUM_NETWORK } from 'src/logic/wallets/getWeb3' import { ETHEREUM_NETWORK } from 'src/config/networks/network.d'
import { networkSelector } from 'src/logic/wallets/store/selectors' import { networkSelector } from 'src/logic/wallets/store/selectors'
import { SAFELIST_ADDRESS, WELCOME_ADDRESS } from 'src/routes/routes' import { SAFELIST_ADDRESS, WELCOME_ADDRESS } from 'src/routes/routes'
import { safeNameSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' import { safeNameSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
@ -26,11 +26,11 @@ import SendModal from 'src/routes/safe/components/Balances/SendModal'
import { useLoadSafe } from 'src/logic/safe/hooks/useLoadSafe' import { useLoadSafe } from 'src/logic/safe/hooks/useLoadSafe'
import { useSafeScheduledUpdates } from 'src/logic/safe/hooks/useSafeScheduledUpdates' import { useSafeScheduledUpdates } from 'src/logic/safe/hooks/useSafeScheduledUpdates'
import useSafeActions from 'src/logic/safe/hooks/useSafeActions' import useSafeActions from 'src/logic/safe/hooks/useSafeActions'
import { currentCurrencySelector, safeFiatBalancesTotalSelector } from 'src/logic/currencyValues/store/selectors/index' import { currentCurrencySelector, safeFiatBalancesTotalSelector } from 'src/logic/currencyValues/store/selectors'
import { formatAmountInUsFormat } from 'src/logic/tokens/utils/formatAmount' import { formatAmountInUsFormat } from 'src/logic/tokens/utils/formatAmount'
import { grantedSelector } from 'src/routes/safe/container/selector' import { grantedSelector } from 'src/routes/safe/container/selector'
import Receive from './ReceiveModal' import ReceiveModal from './ReceiveModal'
import { useSidebarItems } from 'src/components/AppLayout/Sidebar/useSidebarItems' import { useSidebarItems } from 'src/components/AppLayout/Sidebar/useSidebarItems'
const notificationStyles = { const notificationStyles = {
@ -46,6 +46,12 @@ const notificationStyles = {
info: { info: {
background: '#fff', background: '#fff',
}, },
receiveModal: {
height: 'auto',
maxWidth: 'calc(100% - 30px)',
minHeight: '544px',
overflow: 'hidden',
},
} }
const Frame = styled.div` const Frame = styled.div`
@ -55,7 +61,7 @@ const Frame = styled.div`
max-width: 100%; max-width: 100%;
` `
const desiredNetwork = getNetwork() const desiredNetwork = getNetworkId()
const useStyles = makeStyles(notificationStyles) const useStyles = makeStyles(notificationStyles)
@ -67,7 +73,7 @@ const App: React.FC = ({ children }) => {
const matchSafe = useRouteMatch({ path: `${SAFELIST_ADDRESS}`, strict: false }) const matchSafe = useRouteMatch({ path: `${SAFELIST_ADDRESS}`, strict: false })
const history = useHistory() const history = useHistory()
const safeAddress = useSelector(safeParamAddressFromStateSelector) const safeAddress = useSelector(safeParamAddressFromStateSelector)
const safeName = useSelector(safeNameSelector) const safeName = useSelector(safeNameSelector) ?? ''
const { safeActionsState, onShow, onHide, showSendFunds, hideSendFunds } = useSafeActions() const { safeActionsState, onShow, onHide, showSendFunds, hideSendFunds } = useSafeActions()
const currentSafeBalance = useSelector(safeFiatBalancesTotalSelector) const currentSafeBalance = useSelector(safeFiatBalancesTotalSelector)
const currentCurrency = useSelector(currentCurrencySelector) const currentCurrency = useSelector(currentCurrencySelector)
@ -77,7 +83,7 @@ const App: React.FC = ({ children }) => {
useLoadSafe(safeAddress) useLoadSafe(safeAddress)
useSafeScheduledUpdates(safeAddress) useSafeScheduledUpdates(safeAddress)
const sendFunds = safeActionsState.sendFunds as { isOpen: boolean; selectedToken: string } const sendFunds = safeActionsState.sendFunds
const formattedTotalBalance = currentSafeBalance ? formatAmountInUsFormat(currentSafeBalance) : '' const formattedTotalBalance = currentSafeBalance ? formatAmountInUsFormat(currentSafeBalance) : ''
const balance = const balance =
!!formattedTotalBalance && !!currentCurrency ? `${formattedTotalBalance} ${currentCurrency}` : undefined !!formattedTotalBalance && !!currentCurrency ? `${formattedTotalBalance} ${currentCurrency}` : undefined
@ -138,10 +144,11 @@ const App: React.FC = ({ children }) => {
<Modal <Modal
description="Receive Tokens Form" description="Receive Tokens Form"
handleClose={onReceiveHide} handleClose={onReceiveHide}
open={safeActionsState.showReceive as boolean} open={safeActionsState.showReceive}
paperClassName={classes.receiveModal}
title="Receive Tokens" title="Receive Tokens"
> >
<Receive onClose={onReceiveHide} safeAddress={safeAddress} safeName={safeName} /> <ReceiveModal onClose={onReceiveHide} safeAddress={safeAddress} safeName={safeName} />
</Modal> </Modal>
)} )}
</> </>

View File

@ -1,86 +1,27 @@
import { withStyles } from '@material-ui/core/styles'
import Dot from '@material-ui/icons/FiberManualRecord'
import * as React from 'react' import * as React from 'react'
import { getNetworkInfo } from 'src/config'
import Block from 'src/components/layout/Block' type Props = {
import Img from 'src/components/layout/Img' className: string
import { border, fancy, screenSm, warning } from 'src/theme/variables'
const key = require('../assets/key.svg')
const triangle = require('../assets/triangle.svg')
const styles = () => ({
root: {
display: 'none',
[`@media (min-width: ${screenSm}px)`]: {
display: 'flex',
},
},
dot: {
position: 'relative',
backgroundColor: '#ffffff',
color: fancy,
},
key: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: border,
},
warning: {
position: 'relative',
top: '-2px',
},
})
const buildKeyStyleFrom = (size, center, dotSize) => ({
width: `${size}px`,
height: `${size}px`,
marginLeft: center ? `${dotSize}px` : 'none',
borderRadius: `${size}px`,
})
const buildDotStyleFrom = (size, top, right, mode) => ({
width: `${size}px`,
height: `${size}px`,
borderRadius: `${size}px`,
top: `${top}px`,
right: `${right}px`,
color: mode === 'error' ? fancy : warning,
})
const KeyRing = ({
center = false,
circleSize,
classes,
dotRight,
dotSize,
dotTop,
hideDot = false,
keySize,
mode,
}) => {
const keyStyle = buildKeyStyleFrom(circleSize, center, dotSize)
const dotStyle = buildDotStyleFrom(dotSize, dotTop, dotRight, mode)
const isWarning = mode === 'warning'
const img = isWarning ? triangle : key
return (
<>
<Block className={classes.root}>
<Block className={classes.key} style={keyStyle}>
<Img
alt="Status connection"
className={isWarning ? classes.warning : undefined}
height={keySize}
src={img}
width={isWarning ? keySize + 2 : keySize}
/>
</Block>
{!hideDot && <Dot className={classes.dot} style={dotStyle} />}
</Block>
</>
)
} }
export default withStyles(styles as any)(KeyRing) export const CircleDot = (props: Props): React.ReactElement => {
const networkInfo = getNetworkInfo()
return (
<div className={props.className}>
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 10 10">
<circle
cx="208"
cy="203"
r="3"
fill="none"
fillRule="evenodd"
stroke={networkInfo?.backgroundColor ?? '#FF685E'}
strokeWidth="3"
transform="translate(-203 -198)"
/>
</svg>
</div>
)
}

View File

@ -0,0 +1,97 @@
import { createStyles, makeStyles } from '@material-ui/core/styles'
import Dot from '@material-ui/icons/FiberManualRecord'
import * as React from 'react'
import Block from 'src/components/layout/Block'
import Img from 'src/components/layout/Img'
import { border, fancy, screenSm, warning } from 'src/theme/variables'
const key = require('../assets/key.svg')
const triangle = require('../assets/triangle.svg')
const styles = createStyles({
root: {
display: 'none',
[`@media (min-width: ${screenSm}px)`]: {
display: 'flex',
},
},
dot: {
position: 'relative',
backgroundColor: '#ffffff',
color: fancy,
},
key: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: border,
},
warning: {
position: 'relative',
top: '-2px',
},
})
const useStyles = makeStyles(styles)
const buildKeyStyleFrom = (size, center, dotSize) => ({
width: `${size}px`,
height: `${size}px`,
marginLeft: center ? `${dotSize}px` : 'none',
borderRadius: `${size}px`,
})
const buildDotStyleFrom = (size, top, right, mode) => ({
width: `${size}px`,
height: `${size}px`,
borderRadius: `${size}px`,
top: `${top}px`,
right: `${right}px`,
color: mode === 'error' ? fancy : warning,
})
type Props = {
center?: boolean
circleSize?: number
dotRight?: number
dotSize?: number
dotTop?: number
hideDot?: boolean
keySize: number
mode?: string
}
export const KeyRing = ({
center = false,
circleSize,
dotRight,
dotSize,
dotTop,
hideDot = false,
keySize,
mode,
}: Props): React.ReactElement => {
const classes = useStyles(styles)
const keyStyle = buildKeyStyleFrom(circleSize, center, dotSize)
const dotStyle = buildDotStyleFrom(dotSize, dotTop, dotRight, mode)
const isWarning = mode === 'warning'
const img = isWarning ? triangle : key
return (
<>
<Block className={classes.root}>
<Block className={classes.key} style={keyStyle}>
<Img
alt="Status connection"
className={isWarning ? classes.warning : undefined}
height={keySize}
src={img}
width={isWarning ? keySize + 2 : keySize}
/>
</Block>
{!hideDot && <Dot className={classes.dot} style={dotStyle} />}
</Block>
</>
)
}

View File

@ -3,11 +3,10 @@ import * as React from 'react'
import Col from 'src/components/layout/Col' import Col from 'src/components/layout/Col'
import Paragraph from 'src/components/layout/Paragraph' import Paragraph from 'src/components/layout/Paragraph'
import { getNetwork } from 'src/config' import { getNetworkInfo } from 'src/config'
import { border, md, screenSm, sm, xs } from 'src/theme/variables' import { border, md, screenSm, sm, xs, fontColor } from 'src/theme/variables'
const interfaceNetwork = getNetwork() const networkInfo = getNetworkInfo()
const formatNetwork = (network: string): string => network[0].toUpperCase() + network.substring(1).toLowerCase()
const useStyles = makeStyles({ const useStyles = makeStyles({
container: { container: {
@ -19,7 +18,8 @@ const useStyles = makeStyles({
}, },
}, },
text: { text: {
background: border, backgroundColor: `${networkInfo?.backgroundColor ?? border}`,
color: `${networkInfo?.textColor ?? fontColor}`,
borderRadius: '3px', borderRadius: '3px',
lineHeight: 'normal', lineHeight: 'normal',
margin: '0', margin: '0',
@ -31,18 +31,13 @@ const useStyles = makeStyles({
}, },
}) })
interface NetworkLabelProps { const NetworkLabel = (): React.ReactElement => {
network?: string
}
const NetworkLabel = ({ network = interfaceNetwork }: NetworkLabelProps): React.ReactElement => {
const classes = useStyles() const classes = useStyles()
const formattedNetwork = formatNetwork(network)
return ( return (
<Col className={classes.container} middle="xs" start="xs"> <Col className={classes.container} middle="xs" start="xs">
<Paragraph className={classes.text} size="xs"> <Paragraph className={classes.text} size="xs">
{formattedNetwork} {networkInfo.label}
</Paragraph> </Paragraph>
</Col> </Col>
) )

View File

@ -2,11 +2,12 @@ import { withStyles } from '@material-ui/core/styles'
import * as React from 'react' import * as React from 'react'
import ConnectButton from 'src/components/ConnectButton' import ConnectButton from 'src/components/ConnectButton'
import CircleDot from 'src/components/AppLayout/Header/components/CircleDot'
import Block from 'src/components/layout/Block' import Block from 'src/components/layout/Block'
import Paragraph from 'src/components/layout/Paragraph' import Paragraph from 'src/components/layout/Paragraph'
import Row from 'src/components/layout/Row' import Row from 'src/components/layout/Row'
import { lg, md } from 'src/theme/variables' import { lg, md } from 'src/theme/variables'
import { KeyRing } from 'src/components/AppLayout/Header/components/KeyRing'
const styles = () => ({ const styles = () => ({
container: { container: {
@ -42,7 +43,7 @@ const ConnectDetails = ({ classes }) => (
</Row> </Row>
</div> </div>
<Row className={classes.logo} margin="lg"> <Row className={classes.logo} margin="lg">
<CircleDot center circleSize={75} dotRight={25} dotSize={25} dotTop={50} keySize={32} mode="error" /> <KeyRing center circleSize={75} dotRight={25} dotSize={25} dotTop={50} keySize={32} mode="error" />
</Row> </Row>
<Block className={classes.connect}> <Block className={classes.connect}>
<ConnectButton data-testid="heading-connect-btn" /> <ConnectButton data-testid="heading-connect-btn" />

View File

@ -1,10 +1,9 @@
import { withStyles } from '@material-ui/core/styles' import { makeStyles } from '@material-ui/core/styles'
import Dot from '@material-ui/icons/FiberManualRecord' import Dot from '@material-ui/icons/FiberManualRecord'
import classNames from 'classnames' import classNames from 'classnames'
import * as React from 'react' import * as React from 'react'
import { EthHashInfo, Identicon } from '@gnosis.pm/safe-react-components' import { EthHashInfo, Identicon } from '@gnosis.pm/safe-react-components'
import CircleDot from 'src/components/AppLayout/Header/components/CircleDot'
import Spacer from 'src/components/Spacer' import Spacer from 'src/components/Spacer'
import Block from 'src/components/layout/Block' import Block from 'src/components/layout/Block'
import Button from 'src/components/layout/Button' import Button from 'src/components/layout/Button'
@ -14,11 +13,15 @@ import Paragraph from 'src/components/layout/Paragraph'
import Row from 'src/components/layout/Row' import Row from 'src/components/layout/Row'
import { background, connected as connectedBg, lg, md, sm, warning, xs } from 'src/theme/variables' import { background, connected as connectedBg, lg, md, sm, warning, xs } from 'src/theme/variables'
import { upperFirst } from 'src/utils/css' import { upperFirst } from 'src/utils/css'
import { ETHEREUM_NETWORK } from 'src/config/networks/network.d'
import { getExplorerInfo } from 'src/config'
import { KeyRing } from 'src/components/AppLayout/Header/components/KeyRing'
import { CircleDot } from '../CircleDot'
import { createStyles } from '@material-ui/core'
const dot = require('../../assets/dotRinkeby.svg')
const walletIcon = require('../../assets/wallet.svg') const walletIcon = require('../../assets/wallet.svg')
const styles = () => ({ const styles = createStyles({
container: { container: {
padding: `${md} 12px`, padding: `${md} 12px`,
display: 'flex', display: 'flex',
@ -88,9 +91,29 @@ const styles = () => ({
}, },
}) })
const UserDetails = ({ classes, connected, network, onDisconnect, openDashboard, provider, userAddress }) => { type Props = {
connected: boolean
network: ETHEREUM_NETWORK
onDisconnect: () => void
openDashboard?: (() => void | null) | boolean
provider?: string
userAddress: string
}
const useStyles = makeStyles(styles)
export const UserDetails = ({
connected,
network,
onDisconnect,
openDashboard,
provider,
userAddress,
}: Props): React.ReactElement => {
const status = connected ? 'Connected' : 'Connection error' const status = connected ? 'Connected' : 'Connection error'
const color = connected ? 'primary' : 'warning' const color = connected ? 'primary' : 'warning'
const explorerUrl = getExplorerInfo(userAddress)
const classes = useStyles()
return ( return (
<> <>
@ -99,12 +122,12 @@ const UserDetails = ({ classes, connected, network, onDisconnect, openDashboard,
{connected ? ( {connected ? (
<Identicon address={userAddress || 'random'} size="lg" /> <Identicon address={userAddress || 'random'} size="lg" />
) : ( ) : (
<CircleDot circleSize={75} dotRight={25} dotSize={25} dotTop={50} hideDot keySize={30} mode="warning" /> <KeyRing circleSize={75} dotRight={25} dotSize={25} dotTop={50} hideDot keySize={30} mode="warning" />
)} )}
</Row> </Row>
<Block className={classes.user} justify="center"> <Block className={classes.user} justify="center">
{userAddress ? ( {userAddress ? (
<EthHashInfo hash={userAddress} showCopyBtn showEtherscanBtn shortenHash={4} network={network} /> <EthHashInfo hash={userAddress} showCopyBtn explorerUrl={explorerUrl} shortenHash={4} />
) : ( ) : (
'Address not available' 'Address not available'
)} )}
@ -138,9 +161,9 @@ const UserDetails = ({ classes, connected, network, onDisconnect, openDashboard,
Network Network
</Paragraph> </Paragraph>
<Spacer /> <Spacer />
<Img alt="Network" className={classes.logo} height={14} src={dot} /> <CircleDot className={classes.logo} />
<Paragraph align="right" className={classes.labels} noMargin weight="bolder"> <Paragraph align="right" className={classes.labels} noMargin weight="bolder">
{upperFirst(network)} {upperFirst(ETHEREUM_NETWORK[network])}
</Paragraph> </Paragraph>
</Row> </Row>
<Hairline margin="xs" /> <Hairline margin="xs" />
@ -170,5 +193,3 @@ const UserDetails = ({ classes, connected, network, onDisconnect, openDashboard,
</> </>
) )
} }
export default withStyles(styles as any)(UserDetails)

View File

@ -3,11 +3,11 @@ import * as React from 'react'
import { EthHashInfo, Text } from '@gnosis.pm/safe-react-components' import { EthHashInfo, Text } from '@gnosis.pm/safe-react-components'
import NetworkLabel from '../NetworkLabel' import NetworkLabel from '../NetworkLabel'
import CircleDot from 'src/components/AppLayout/Header/components/CircleDot'
import Col from 'src/components/layout/Col' import Col from 'src/components/layout/Col'
import Paragraph from 'src/components/layout/Paragraph' import Paragraph from 'src/components/layout/Paragraph'
import WalletIcon from '../WalletIcon' import WalletIcon from '../WalletIcon'
import { connected as connectedBg, screenSm, sm } from 'src/theme/variables' import { connected as connectedBg, screenSm, sm } from 'src/theme/variables'
import { KeyRing } from 'src/components/AppLayout/Header/components/KeyRing'
const useStyles = makeStyles({ const useStyles = makeStyles({
network: { network: {
@ -62,16 +62,16 @@ const useStyles = makeStyles({
interface ProviderInfoProps { interface ProviderInfoProps {
connected: boolean connected: boolean
provider: string provider: string
network: string // TODO: [xDai] Review. This may cause some issues with EthHashInfo.
userAddress: string userAddress: string
} }
const ProviderInfo = ({ connected, provider, userAddress, network }: ProviderInfoProps): React.ReactElement => { const ProviderInfo = ({ connected, provider, userAddress }: ProviderInfoProps): React.ReactElement => {
const classes = useStyles() const classes = useStyles()
const addressColor = connected ? 'text' : 'warning' const addressColor = connected ? 'text' : 'warning'
return ( return (
<> <>
{!connected && <CircleDot circleSize={35} dotRight={11} dotSize={16} dotTop={24} keySize={14} mode="warning" />} {!connected && <KeyRing circleSize={35} dotRight={11} dotSize={16} dotTop={24} keySize={14} mode="warning" />}
<WalletIcon provider={provider.toUpperCase()} /> <WalletIcon provider={provider.toUpperCase()} />
<Col className={classes.account} layout="column" start="sm"> <Col className={classes.account} layout="column" start="sm">
<Paragraph <Paragraph
@ -93,7 +93,6 @@ const ProviderInfo = ({ connected, provider, userAddress, network }: ProviderInf
identiconSize="xs" identiconSize="xs"
textColor={addressColor} textColor={addressColor}
textSize="sm" textSize="sm"
network={network}
/> />
) : ( ) : (
<Text size="md" color={addressColor}> <Text size="md" color={addressColor}>
@ -107,7 +106,7 @@ const ProviderInfo = ({ connected, provider, userAddress, network }: ProviderInf
</div> </div>
</Col> </Col>
<Col className={classes.networkLabel} layout="column" start="sm"> <Col className={classes.networkLabel} layout="column" start="sm">
<NetworkLabel network={network} /> <NetworkLabel />
</Col> </Col>
</> </>
) )

View File

@ -1,11 +1,10 @@
import { withStyles } from '@material-ui/core/styles' import { withStyles } from '@material-ui/core/styles'
import * as React from 'react' import * as React from 'react'
import CircleDot from 'src/components/AppLayout/Header/components/CircleDot'
import Col from 'src/components/layout/Col' import Col from 'src/components/layout/Col'
import Paragraph from 'src/components/layout/Paragraph' import Paragraph from 'src/components/layout/Paragraph'
import { sm } from 'src/theme/variables' import { sm } from 'src/theme/variables'
import { KeyRing } from 'src/components/AppLayout/Header/components/KeyRing'
const styles = () => ({ const styles = () => ({
network: { network: {
@ -27,7 +26,7 @@ const styles = () => ({
const ProviderDisconnected = ({ classes }) => ( const ProviderDisconnected = ({ classes }) => (
<> <>
<CircleDot circleSize={35} dotRight={11} dotSize={16} dotTop={24} keySize={17} mode="error" /> <KeyRing circleSize={35} dotRight={11} dotSize={16} dotTop={24} keySize={17} mode="error" />
<Col className={classes.account} end="sm" layout="column" middle="xs"> <Col className={classes.account} end="sm" layout="column" middle="xs">
<Paragraph <Paragraph
className={classes.network} className={classes.network}

View File

@ -3,7 +3,7 @@ import { useSelector, useDispatch } from 'react-redux'
import Layout from './components/Layout' import Layout from './components/Layout'
import ConnectDetails from './components/ProviderDetails/ConnectDetails' import ConnectDetails from './components/ProviderDetails/ConnectDetails'
import UserDetails from './components/ProviderDetails/UserDetails' import { UserDetails } from './components/ProviderDetails/UserDetails'
import ProviderAccessible from './components/ProviderInfo/ProviderAccessible' import ProviderAccessible from './components/ProviderInfo/ProviderAccessible'
import ProviderDisconnected from './components/ProviderInfo/ProviderDisconnected' import ProviderDisconnected from './components/ProviderInfo/ProviderDisconnected'
import { import {
@ -54,7 +54,7 @@ const HeaderComponent = (): React.ReactElement => {
return <ProviderDisconnected /> return <ProviderDisconnected />
} }
return <ProviderAccessible connected={available} provider={provider} network={network} userAddress={userAddress} /> return <ProviderAccessible connected={available} provider={provider} userAddress={userAddress} />
} }
const getProviderDetailsBased = () => { const getProviderDetailsBased = () => {

View File

@ -8,11 +8,13 @@ import {
Identicon, Identicon,
Button, Button,
CopyToClipboardBtn, CopyToClipboardBtn,
EtherscanButton, ExplorerButton,
} from '@gnosis.pm/safe-react-components' } from '@gnosis.pm/safe-react-components'
import { getNetwork } from 'src/config'
import FlexSpacer from 'src/components/FlexSpacer' import FlexSpacer from 'src/components/FlexSpacer'
import { getExplorerInfo, getNetworkInfo } from 'src/config'
import { NetworkSettings } from 'src/config/networks/network.d'
import { border, fontColor } from 'src/theme/variables'
export const TOGGLE_SIDEBAR_BTN_TESTID = 'TOGGLE_SIDEBAR_BTN' export const TOGGLE_SIDEBAR_BTN_TESTID = 'TOGGLE_SIDEBAR_BTN'
@ -46,6 +48,19 @@ const StyledButton = styled(Button)`
margin: 0 4px 0 0; margin: 0 4px 0 0;
} }
` `
type StyledTextLabelProps = {
networkInfo: NetworkSettings
}
const StyledTextLabel = styled(Text)`
margin: -8px 0 4px -8px;
padding: 4px 8px;
width: 100%;
text-align: center;
color: ${(props: StyledTextLabelProps) => props.networkInfo?.textColor ?? fontColor};
background-color: ${(props: StyledTextLabelProps) => props.networkInfo?.backgroundColor ?? border};
`
const StyledEthHashInfo = styled(EthHashInfo)` const StyledEthHashInfo = styled(EthHashInfo)`
p { p {
color: ${({ theme }) => theme.colors.placeHolder}; color: ${({ theme }) => theme.colors.placeHolder};
@ -110,43 +125,50 @@ const SafeHeader = ({
</Container> </Container>
) )
} }
const explorerUrl = getExplorerInfo(address)
const networkInfo = getNetworkInfo()
return ( return (
<Container> <>
<IdenticonContainer> <StyledTextLabel size="sm" networkInfo={networkInfo}>
<FlexSpacer /> {networkInfo.label}
<Identicon address={address} size="lg" /> </StyledTextLabel>
<UnStyledButton onClick={onToggleSafeList} data-testid={TOGGLE_SIDEBAR_BTN_TESTID}> <Container>
<Icon size="md" type="circleDropdown" /> <IdenticonContainer>
</UnStyledButton> <FlexSpacer />
</IdenticonContainer> <Identicon address={address} size="lg" />
<UnStyledButton onClick={onToggleSafeList} data-testid={TOGGLE_SIDEBAR_BTN_TESTID}>
<Icon size="md" type="circleDropdown" />
</UnStyledButton>
</IdenticonContainer>
<Text size="xl">{safeName}</Text> <Text size="xl">{safeName}</Text>
<StyledEthHashInfo hash={address} shortenHash={4} textSize="sm" /> <StyledEthHashInfo hash={address} shortenHash={4} textSize="sm" />
<IconContainer> <IconContainer>
<UnStyledButton onClick={onReceiveClick}> <UnStyledButton onClick={onReceiveClick}>
<Icon size="sm" type="qrCode" tooltip="Show QR" /> <Icon size="sm" type="qrCode" tooltip="Show QR" />
</UnStyledButton> </UnStyledButton>
<CopyToClipboardBtn textToCopy={address} /> <CopyToClipboardBtn textToCopy={address} />
<EtherscanButton value={address} network={getNetwork()} /> <ExplorerButton explorerUrl={explorerUrl} />
</IconContainer> </IconContainer>
{granted ? null : ( {granted ? null : (
<StyledLabel> <StyledLabel>
<Text size="sm" color="white"> <Text size="sm" color="white">
READ ONLY READ ONLY
</Text>
</StyledLabel>
)}
<StyledText size="xl">{balance}</StyledText>
<StyledButton size="md" disabled={!granted} color="primary" variant="contained" onClick={onNewTransactionClick}>
<FixedIcon type="arrowSentWhite" />
<Text size="lg" color="white">
New Transaction
</Text> </Text>
</StyledLabel> </StyledButton>
)} </Container>
</>
<StyledText size="xl">{balance}</StyledText>
<StyledButton size="md" disabled={!granted} color="primary" variant="contained" onClick={onNewTransactionClick}>
<FixedIcon type="arrowSentWhite" />
<Text size="lg" color="white">
New Transaction
</Text>
</StyledButton>
</Container>
) )
} }

View File

@ -4,8 +4,13 @@ import { useRouteMatch } from 'react-router-dom'
import { ListItemType } from 'src/components/List' import { ListItemType } from 'src/components/List'
import ListIcon from 'src/components/List/ListIcon' import ListIcon from 'src/components/List/ListIcon'
import { SAFELIST_ADDRESS } from 'src/routes/routes' import { SAFELIST_ADDRESS } from 'src/routes/routes'
import { FEATURES } from 'src/config/networks/network.d'
import { useSelector } from 'react-redux'
import { safeFeaturesEnabledSelector } from 'src/logic/safe/store/selectors'
const useSidebarItems = (): ListItemType[] => { const useSidebarItems = (): ListItemType[] => {
const featuresEnabled = useSelector(safeFeaturesEnabledSelector)
const safeAppsEnabled = Boolean(featuresEnabled?.includes(FEATURES.SAFE_APPS))
const matchSafe = useRouteMatch({ path: `${SAFELIST_ADDRESS}`, strict: false }) const matchSafe = useRouteMatch({ path: `${SAFELIST_ADDRESS}`, strict: false })
const matchSafeWithAddress = useRouteMatch<{ safeAddress: string }>({ path: `${SAFELIST_ADDRESS}/:safeAddress` }) const matchSafeWithAddress = useRouteMatch<{ safeAddress: string }>({ path: `${SAFELIST_ADDRESS}/:safeAddress` })
const matchSafeWithAction = useRouteMatch({ path: `${SAFELIST_ADDRESS}/:safeAddress/:safeAction` }) as { const matchSafeWithAction = useRouteMatch({ path: `${SAFELIST_ADDRESS}/:safeAddress/:safeAction` }) as {
@ -13,11 +18,30 @@ const useSidebarItems = (): ListItemType[] => {
params: Record<string, string> params: Record<string, string>
} }
const sidebarItems = useMemo((): ListItemType[] => { return useMemo((): ListItemType[] => {
if (!matchSafe || !matchSafeWithAddress) { if (!matchSafe || !matchSafeWithAddress) {
return [] return []
} }
const settingsItem = {
label: 'Settings',
icon: <ListIcon type="settings" />,
selected: matchSafeWithAction?.params.safeAction === 'settings',
href: `${matchSafeWithAddress?.url}/settings`,
}
const safeSidebar = safeAppsEnabled
? [
{
label: 'Apps',
icon: <ListIcon type="apps" />,
selected: matchSafeWithAction?.params.safeAction === 'apps',
href: `${matchSafeWithAddress?.url}/apps`,
},
settingsItem,
]
: [settingsItem]
return [ return [
{ {
label: 'ASSETS', label: 'ASSETS',
@ -37,22 +61,9 @@ const useSidebarItems = (): ListItemType[] => {
selected: matchSafeWithAction?.params.safeAction === 'address-book', selected: matchSafeWithAction?.params.safeAction === 'address-book',
href: `${matchSafeWithAddress?.url}/address-book`, href: `${matchSafeWithAddress?.url}/address-book`,
}, },
{ ...safeSidebar,
label: 'Apps',
icon: <ListIcon type="apps" />,
selected: matchSafeWithAction?.params.safeAction === 'apps',
href: `${matchSafeWithAddress?.url}/apps`,
},
{
label: 'Settings',
icon: <ListIcon type="settings" />,
selected: matchSafeWithAction?.params.safeAction === 'settings',
href: `${matchSafeWithAddress?.url}/settings`,
},
] ]
}, [matchSafe, matchSafeWithAction, matchSafeWithAddress]) }, [matchSafe, matchSafeWithAction, matchSafeWithAddress, safeAppsEnabled])
return sidebarItems
} }
export { useSidebarItems } export { useSidebarItems }

View File

@ -3,15 +3,16 @@ import React from 'react'
import Button from 'src/components/layout/Button' import Button from 'src/components/layout/Button'
import { getNetworkId } from 'src/config' import { getNetworkId } from 'src/config'
import { ETHEREUM_NETWORK } from 'src/config/networks/network.d'
import { getWeb3, setWeb3 } from 'src/logic/wallets/getWeb3' import { getWeb3, setWeb3 } from 'src/logic/wallets/getWeb3'
import { fetchProvider } from 'src/logic/wallets/store/actions' import { fetchProvider } from 'src/logic/wallets/store/actions'
import transactionDataCheck from 'src/logic/wallets/transactionDataCheck' import transactionDataCheck from 'src/logic/wallets/transactionDataCheck'
import { getSupportedWallets } from 'src/logic/wallets/utils/walletList' import { getSupportedWallets } from 'src/logic/wallets/utils/walletList'
import { store } from 'src/store' import { store } from 'src/store'
import { BLOCKNATIVE_KEY } from 'src/utils/constants'
const isMainnet = process.env.REACT_APP_NETWORK === 'mainnet' const networkId = getNetworkId()
const BLOCKNATIVE_API_KEY = BLOCKNATIVE_KEY[networkId] ?? BLOCKNATIVE_KEY[ETHEREUM_NETWORK.RINKEBY]
const BLOCKNATIVE_API_KEY = isMainnet ? process.env.REACT_APP_BLOCKNATIVE_KEY : '7fbb9cee-7e97-4436-8770-8b29a9a8814c'
let lastUsedAddress = '' let lastUsedAddress = ''
let providerName let providerName
@ -20,7 +21,7 @@ const wallets = getSupportedWallets()
export const onboard = Onboard({ export const onboard = Onboard({
dappId: BLOCKNATIVE_API_KEY, dappId: BLOCKNATIVE_API_KEY,
networkId: getNetworkId(), networkId: networkId,
subscriptions: { subscriptions: {
wallet: (wallet) => { wallet: (wallet) => {
if (wallet.provider) { if (wallet.provider) {

View File

@ -6,8 +6,8 @@ import React from 'react'
import EtherscanOpenIcon from './img/etherscan-open.svg' import EtherscanOpenIcon from './img/etherscan-open.svg'
import Img from 'src/components/layout/Img' import Img from 'src/components/layout/Img'
import { getEtherScanLink } from 'src/logic/wallets/getWeb3'
import { xs } from 'src/theme/variables' import { xs } from 'src/theme/variables'
import { getExplorerInfo } from 'src/config'
const useStyles = makeStyles({ const useStyles = makeStyles({
container: { container: {
@ -30,26 +30,23 @@ const useStyles = makeStyles({
interface EtherscanBtnProps { interface EtherscanBtnProps {
className?: string className?: string
increaseZindex?: boolean increaseZindex?: boolean
type: 'tx' | 'address'
value: string value: string
} }
const EtherscanBtn = ({ const EtherscanBtn = ({ className = '', increaseZindex = false, value }: EtherscanBtnProps): React.ReactElement => {
className = '',
increaseZindex = false,
type,
value,
}: EtherscanBtnProps): React.ReactElement => {
const classes = useStyles() const classes = useStyles()
const customClasses = increaseZindex ? { popper: classes.increasedPopperZindex } : {} const customClasses = increaseZindex ? { popper: classes.increasedPopperZindex } : {}
const explorerInfo = getExplorerInfo(value)
const { url } = explorerInfo()
return ( return (
<Tooltip classes={customClasses} placement="top" title="Show details on Etherscan"> <Tooltip classes={customClasses} placement="top" title="Show details on Etherscan">
<a <a
aria-label="Show details on Etherscan" aria-label="Show details on Etherscan"
className={cn(classes.container, className)} className={cn(classes.container, className)}
onClick={(event) => event.stopPropagation()} onClick={(event) => event.stopPropagation()}
href={getEtherScanLink(type, value)} href={url}
rel="noopener noreferrer" rel="noopener noreferrer"
target="_blank" target="_blank"
> >

View File

@ -17,11 +17,10 @@ interface EtherscanLinkProps {
className?: string className?: string
cut?: number cut?: number
knownAddress?: boolean knownAddress?: boolean
type: 'tx' | 'address'
value: string value: string
} }
const EtherscanLink = ({ className, cut, knownAddress, type, value }: EtherscanLinkProps): React.ReactElement => { const EtherscanLink = ({ className, cut, knownAddress, value }: EtherscanLinkProps): React.ReactElement => {
const classes = useStyles() const classes = useStyles()
return ( return (
@ -30,7 +29,7 @@ const EtherscanLink = ({ className, cut, knownAddress, type, value }: EtherscanL
{cut ? shortVersionOf(value, cut) : value} {cut ? shortVersionOf(value, cut) : value}
</Span> </Span>
<CopyBtn className={cn(classes.button, classes.firstButton)} content={value} /> <CopyBtn className={cn(classes.button, classes.firstButton)} content={value} />
<EtherscanBtn className={classes.button} type={type} value={value} /> <EtherscanBtn className={classes.button} value={value} />
{knownAddress !== undefined ? <EllipsisTransactionDetails address={value} knownAddress={knownAddress} /> : null} {knownAddress !== undefined ? <EllipsisTransactionDetails address={value} knownAddress={knownAddress} /> : null}
</Block> </Block>
) )

View File

@ -3,7 +3,7 @@ import styled from 'styled-components'
export const Wrapper = styled.div` export const Wrapper = styled.div`
display: grid; display: grid;
grid-template-columns: 245px auto; grid-template-columns: 245px auto;
min-height: 560px; min-height: 75vh;
.background { .background {
box-shadow: 1px 2px 10px 0 rgba(212, 212, 211, 0.59); box-shadow: 1px 2px 10px 0 rgba(212, 212, 211, 0.59);
background-color: white; background-color: white;

View File

@ -0,0 +1,81 @@
import React from 'react'
import styled from 'styled-components'
import { ButtonLink, EthHashInfo, Text } from '@gnosis.pm/safe-react-components'
import { formatAmount } from 'src/logic/tokens/utils/formatAmount'
import { sameAddress } from 'src/logic/wallets/ethAddresses'
import DefaultBadge from './DefaultBadge'
import { SafeRecordProps } from 'src/logic/safe/store/models/safe'
import { DefaultSafe } from 'src/routes/safe/store/reducer/types/safe'
import { SetDefaultSafe } from 'src/logic/safe/store/actions/setDefaultSafe'
import { makeStyles } from '@material-ui/core/styles'
import { getNetworkInfo } from 'src/config'
const StyledButtonLink = styled(ButtonLink)`
visibility: hidden;
white-space: nowrap;
`
const useStyles = makeStyles({
wrapper: {
display: 'flex',
padding: '5px 0',
width: '100%',
justifyContent: 'space-between',
'& > nth-child(2)': {
display: 'flex',
alignItems: 'center',
},
},
addressDetails: {
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
width: '175px',
'& div': {
marginLeft: '0px',
padding: '5px 20px',
'& img': {
marginRight: '5px',
},
'& p': {
marginTop: '3px',
},
},
},
})
type Props = {
safe: SafeRecordProps
defaultSafe: DefaultSafe
setDefaultSafe: SetDefaultSafe
}
const { nativeCoin } = getNetworkInfo()
export const AddressWrapper = (props: Props): React.ReactElement => {
const classes = useStyles()
const { safe, defaultSafe, setDefaultSafe } = props
return (
<div className={classes.wrapper}>
<EthHashInfo hash={safe.address} name={safe.name} showIdenticon shortenHash={4} />
<div className={classes.addressDetails}>
<Text size="xl">{`${formatAmount(safe.ethBalance)} ${nativeCoin.name}`}</Text>
{sameAddress(defaultSafe, safe.address) ? (
<DefaultBadge />
) : (
<StyledButtonLink
className="safeListMakeDefaultButton"
textSize="sm"
onClick={() => {
setDefaultSafe(safe.address)
}}
color="primary"
>
Make default
</StyledButtonLink>
)}
</div>
</div>
)
}

View File

@ -1,62 +1,22 @@
import MuiList from '@material-ui/core/List' import MuiList from '@material-ui/core/List'
import ListItem from '@material-ui/core/ListItem' import ListItem from '@material-ui/core/ListItem'
import { makeStyles } from '@material-ui/core/styles' import { makeStyles } from '@material-ui/core/styles'
import { EthHashInfo, Icon, Text, ButtonLink } from '@gnosis.pm/safe-react-components' import { Icon } from '@gnosis.pm/safe-react-components'
import * as React from 'react' import * as React from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { SafeRecord } from 'src/logic/safe/store/models/safe' import { SafeRecord } from 'src/logic/safe/store/models/safe'
import { DefaultSafe } from 'src/routes/safe/store/reducer/types/safe' import { DefaultSafe } from 'src/routes/safe/store/reducer/types/safe'
import { SetDefaultSafe } from 'src/logic/safe/store/actions/setDefaultSafe' import { SetDefaultSafe } from 'src/logic/safe/store/actions/setDefaultSafe'
import { getNetwork } from 'src/config'
import DefaultBadge from './DefaultBadge'
import Hairline from 'src/components/layout/Hairline' import Hairline from 'src/components/layout/Hairline'
import Link from 'src/components/layout/Link' import Link from 'src/components/layout/Link'
import { formatAmount } from 'src/logic/tokens/utils/formatAmount'
import { sameAddress } from 'src/logic/wallets/ethAddresses' import { sameAddress } from 'src/logic/wallets/ethAddresses'
import { SAFELIST_ADDRESS } from 'src/routes/routes' import { SAFELIST_ADDRESS } from 'src/routes/routes'
import { AddressWrapper } from './AddresWrapper'
export const SIDEBAR_SAFELIST_ROW_TESTID = 'SIDEBAR_SAFELIST_ROW_TESTID' export const SIDEBAR_SAFELIST_ROW_TESTID = 'SIDEBAR_SAFELIST_ROW_TESTID'
const StyledIcon = styled(Icon)` const StyledIcon = styled(Icon)`
margin-right: 4px; margin-right: 4px;
` `
const AddressWrapper = styled.div`
display: flex;
padding: 5px 0;
width: 100%;
justify-content: space-between;
> nth-child(2) {
display: flex;
align-items: center;
}
`
const StyledButtonLink = styled(ButtonLink)`
visibility: hidden;
white-space: nowrap;
`
const AddressDetails = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
width: 175px;
div {
margin-left: 0px;
padding: 5px 20px;
img {
margin-right: 5px;
}
p {
margin-top: 3px;
}
}
`
const useStyles = makeStyles({ const useStyles = makeStyles({
list: { list: {
@ -107,34 +67,7 @@ const SafeList = ({ currentSafe, defaultSafe, onSafeClick, safes, setDefaultSafe
) : ( ) : (
<div className={classes.noIcon}>placeholder</div> <div className={classes.noIcon}>placeholder</div>
)} )}
<AddressWrapper safe={safe} defaultSafe={defaultSafe} setDefaultSafe={setDefaultSafe} />
<AddressWrapper>
<EthHashInfo
hash={safe.address}
name={safe.name}
showIdenticon
shortenHash={4}
network={getNetwork()}
/>
<AddressDetails>
<Text size="xl">{`${formatAmount(safe.ethBalance)} ETH`}</Text>
{sameAddress(defaultSafe, safe.address) ? (
<DefaultBadge />
) : (
<StyledButtonLink
className="safeListMakeDefaultButton"
textSize="sm"
onClick={() => {
setDefaultSafe(safe.address)
}}
color="primary"
>
Make default
</StyledButtonLink>
)}
</AddressDetails>
</AddressWrapper>
</ListItem> </ListItem>
</Link> </Link>
<Hairline /> <Hairline />

View File

@ -7,6 +7,7 @@ import { Validator, composeValidators, mustBeEthereumAddress, required } from 's
import { trimSpaces } from 'src/utils/strings' import { trimSpaces } from 'src/utils/strings'
import { getAddressFromENS } from 'src/logic/wallets/getWeb3' import { getAddressFromENS } from 'src/logic/wallets/getWeb3'
import { isValidEnsName } from 'src/logic/wallets/ethAddresses' import { isValidEnsName } from 'src/logic/wallets/ethAddresses'
import { checksumAddress } from 'src/utils/checksumAddress'
// an idea for second field was taken from here // an idea for second field was taken from here
// https://github.com/final-form/react-final-form-listeners/blob/master/src/OnBlur.js // https://github.com/final-form/react-final-form-listeners/blob/master/src/OnBlur.js
@ -56,11 +57,15 @@ const AddressInput = ({
if (isValidEnsName(address)) { if (isValidEnsName(address)) {
try { try {
const resolverAddr = await getAddressFromENS(address) const resolverAddr = await getAddressFromENS(address)
fieldMutator(resolverAddr) const formattedAddress = checksumAddress(resolverAddr)
fieldMutator(formattedAddress)
} catch (err) { } catch (err) {
console.error('Failed to resolve address for ENS name: ', err) console.error('Failed to resolve address for ENS name: ', err)
} }
} else fieldMutator(address) } else {
const formattedAddress = checksumAddress(address)
fieldMutator(formattedAddress)
}
}} }}
</OnChange> </OnChange>
</> </>

View File

@ -1,11 +1,17 @@
import classNames from 'classnames/bind' import classNames from 'classnames/bind'
import React from 'react' import React, { ReactElement, ImgHTMLAttributes } from 'react'
import styles from './index.module.scss' import styles from './index.module.scss'
const cx = classNames.bind(styles) const cx = classNames.bind(styles)
const Img: any = ({ alt, bordered, className, fullwidth, style, testId = '', ...props }) => { type ImgProps = ImgHTMLAttributes<HTMLImageElement> & {
bordered?: boolean
fullwidth?: boolean
testId?: string
}
const Img = ({ alt, bordered, className, fullwidth, style, testId = '', ...props }: ImgProps): ReactElement => {
const classes = cx(styles.img, { fullwidth, bordered }, className) const classes = cx(styles.img, { fullwidth, bordered }, className)
return <img alt={alt} className={classes} data-testid={testId} style={style} {...props} /> return <img alt={alt} className={classes} data-testid={testId} style={style} {...props} />

View File

@ -0,0 +1,136 @@
import { ETHEREUM_NETWORK } from 'src/config/networks/network.d'
import { default as networks } from 'src/config/networks'
const { mainnet, xdai } = networks
describe('Config Services', () => {
beforeEach(() => {
jest.resetModules()
})
it(`should load 'test' network config`, () => {
// Given
jest.mock('src/utils/constants', () => ({
NODE_ENV: 'test',
}))
const { getNetworkInfo } = require('src/config')
// When
const networkInfo = getNetworkInfo()
// Then
expect(networkInfo.id).toBe(ETHEREUM_NETWORK.LOCAL)
})
it(`should load 'mainnet' network config`, () => {
// Given
jest.mock('src/utils/constants', () => ({
NODE_ENV: '',
NETWORK: 'MAINNET',
}))
const { getNetworkInfo } = require('src/config')
// When
const networkInfo = getNetworkInfo()
// Then
expect(networkInfo.id).toBe(ETHEREUM_NETWORK.MAINNET)
})
it(`should load 'mainnet.dev' network config`, () => {
// Given
jest.mock('src/utils/constants', () => ({
NODE_ENV: '',
NETWORK: 'MAINNET',
}))
const { getTxServiceUrl, getGnosisSafeAppsUrl } = require('src/config')
const TX_SERVICE_URL = mainnet.environment.dev?.txServiceUrl
const SAFE_APPS_URL = mainnet.environment.dev?.safeAppsUrl
// When
const txServiceUrl = getTxServiceUrl()
const safeAppsUrl = getGnosisSafeAppsUrl()
// Then
expect(TX_SERVICE_URL).toBe(txServiceUrl)
expect(SAFE_APPS_URL).toBe(safeAppsUrl)
})
it(`should load 'mainnet.staging' network config`, () => {
// Given
jest.mock('src/utils/constants', () => ({
NODE_ENV: 'production',
NETWORK: 'MAINNET',
}))
const { getTxServiceUrl, getGnosisSafeAppsUrl } = require('src/config')
const TX_SERVICE_URL = mainnet.environment.staging?.txServiceUrl
const SAFE_APPS_URL = mainnet.environment.staging?.safeAppsUrl
// When
const txServiceUrl = getTxServiceUrl()
const safeAppsUrl = getGnosisSafeAppsUrl()
// Then
expect(TX_SERVICE_URL).toBe(txServiceUrl)
expect(SAFE_APPS_URL).toBe(safeAppsUrl)
})
it(`should load 'mainnet.production' network config`, () => {
// Given
jest.mock('src/utils/constants', () => ({
NODE_ENV: 'production',
NETWORK: 'MAINNET',
APP_ENV: 'production'
}))
const { getTxServiceUrl, getGnosisSafeAppsUrl } = require('src/config')
const TX_SERVICE_URL = mainnet.environment.production.txServiceUrl
const SAFE_APPS_URL = mainnet.environment.production.safeAppsUrl
// When
const txServiceUrl = getTxServiceUrl()
const safeAppsUrl = getGnosisSafeAppsUrl()
// Then
expect(TX_SERVICE_URL).toBe(txServiceUrl)
expect(SAFE_APPS_URL).toBe(safeAppsUrl)
})
it(`should load 'xdai.production' network config`, () => {
// Given
jest.mock('src/utils/constants', () => ({
NODE_ENV: 'production',
NETWORK: 'XDAI',
APP_ENV: 'production'
}))
const { getTxServiceUrl, getGnosisSafeAppsUrl } = require('src/config')
const TX_SERVICE_URL = xdai.environment.production.txServiceUrl
const SAFE_APPS_URL = xdai.environment.production.safeAppsUrl
// When
const txServiceUrl = getTxServiceUrl()
const safeAppsUrl = getGnosisSafeAppsUrl()
// Then
expect(TX_SERVICE_URL).toBe(txServiceUrl)
expect(SAFE_APPS_URL).toBe(safeAppsUrl)
})
it(`should default to 'xdai.production' network config if no environment is found`, () => {
// Given
jest.mock('src/utils/constants', () => ({
NODE_ENV: '',
NETWORK: 'XDAI',
}))
const { getTxServiceUrl, getGnosisSafeAppsUrl } = require('src/config')
const TX_SERVICE_URL = xdai.environment.production.txServiceUrl
const SAFE_APPS_URL = xdai.environment.production.safeAppsUrl
// When
const txServiceUrl = getTxServiceUrl()
const safeAppsUrl = getGnosisSafeAppsUrl()
// Then
expect(TX_SERVICE_URL).toBe(txServiceUrl)
expect(SAFE_APPS_URL).toBe(safeAppsUrl)
})
})

View File

@ -1,11 +0,0 @@
//
import devConfig from './development'
import { TX_SERVICE_HOST, RELAY_API_URL } from 'src/config/names'
const devMainnetConfig = {
...devConfig,
[TX_SERVICE_HOST]: 'https://safe-transaction.mainnet.staging.gnosisdev.com/api/v1/',
[RELAY_API_URL]: 'https://safe-relay.mainnet.staging.gnosisdev.com/api/v1/',
}
export default devMainnetConfig

View File

@ -1,11 +0,0 @@
import { TX_SERVICE_HOST, SIGNATURES_VIA_METAMASK, RELAY_API_URL, SAFE_APPS_URL } from 'src/config/names'
const devConfig = {
[TX_SERVICE_HOST]: 'https://safe-transaction.staging.gnosisdev.com/api/v1/',
[SIGNATURES_VIA_METAMASK]: false,
[RELAY_API_URL]: 'https://safe-relay.staging.gnosisdev.com/api/v1/',
[SAFE_APPS_URL]: 'https://safe-apps.dev.gnosisdev.com/'
//[SAFE_APPS_URL]: 'http://localhost:3002/'
}
export default devConfig

View File

@ -1,103 +1,165 @@
import { checksumAddress } from 'src/utils/checksumAddress'; import networks from 'src/config/networks'
import { EnvironmentSettings, ETHEREUM_NETWORK, NetworkSettings, SafeFeatures } from 'src/config/networks/network.d'
import { APP_ENV, ETHERSCAN_API_KEY, GOOGLE_ANALYTICS_ID, INFURA_TOKEN, NETWORK, NODE_ENV } from 'src/utils/constants'
import { ensureOnce } from 'src/utils/singleton' import { ensureOnce } from 'src/utils/singleton'
import { ETHEREUM_NETWORK } from 'src/logic/wallets/getWeb3' import memoize from 'lodash.memoize'
import {
RELAY_API_URL,
SIGNATURES_VIA_METAMASK,
TX_SERVICE_HOST,
SAFE_APPS_URL
} from 'src/config/names'
import devConfig from './development'
import testConfig from './testing'
import stagingConfig from './staging'
import prodConfig from './production'
import mainnetDevConfig from './development-mainnet'
import mainnetProdConfig from './production-mainnet'
import mainnetStagingConfig from './staging-mainnet'
const configuration = () => { export const getNetworkId = (): ETHEREUM_NETWORK => ETHEREUM_NETWORK[NETWORK]
if (process.env.NODE_ENV === 'test') {
return testConfig export const getNetworkName = (): string => ETHEREUM_NETWORK[getNetworkId()]
const getCurrentEnvironment = (): string => {
switch (NODE_ENV) {
case 'test': {
return 'test'
}
case 'production': {
return APP_ENV === 'production' ? 'production' : 'staging'
}
default: {
return 'dev'
}
}
}
type NetworkSpecificConfiguration = EnvironmentSettings & {
network: NetworkSettings,
disabledFeatures?: SafeFeatures,
}
const configuration = (): NetworkSpecificConfiguration => {
const currentEnvironment = getCurrentEnvironment()
// special case for test environment
if (currentEnvironment === 'test') {
const configFile = networks.local
return {
...configFile.environment.production,
network: configFile.network,
disabledFeatures: configFile.disabledFeatures,
}
} }
if (process.env.NODE_ENV === 'production') { // lookup the config file based on the network specified in the NETWORK variable
if (process.env.REACT_APP_NETWORK === 'mainnet') { const configFile = networks[getNetworkName().toLowerCase()]
return process.env.REACT_APP_ENV === 'production' // defaults to 'production' as it's the only environment that is required for the network configs
? mainnetProdConfig const networkBaseConfig = configFile.environment[currentEnvironment] ?? configFile.environment.production
: mainnetStagingConfig
return {
...networkBaseConfig,
network: configFile.network,
disabledFeatures: configFile.disabledFeatures,
}
}
const getConfig: () => NetworkSpecificConfiguration = ensureOnce(configuration)
export const getTxServiceUrl = (): string => getConfig()?.txServiceUrl
export const getRelayUrl = (): string | undefined => getConfig()?.relayApiUrl
export const getGnosisSafeAppsUrl = (): string => getConfig()?.safeAppsUrl
export const getRpcServiceUrl = (): string => {
const usesInfuraRPC = [ETHEREUM_NETWORK.MAINNET, ETHEREUM_NETWORK.RINKEBY].includes(getNetworkId())
if (usesInfuraRPC) {
return `${getConfig()?.rpcServiceUrl}/${INFURA_TOKEN}`
}
return getConfig()?.rpcServiceUrl
}
export const getNetworkExplorerInfo = (): { name: string; url: string; apiUrl: string } => ({
name: getConfig()?.networkExplorerName,
url: getConfig()?.networkExplorerUrl,
apiUrl: getConfig()?.networkExplorerApiUrl,
})
export const getNetworkConfigDisabledFeatures = (): SafeFeatures => getConfig()?.disabledFeatures || []
export const getNetworkInfo = (): NetworkSettings => getConfig()?.network
export const getTxServiceUriFrom = (safeAddress: string) => `/safes/${safeAddress}/transactions/`
export const getIncomingTxServiceUriTo = (safeAddress: string) => `/safes/${safeAddress}/incoming-transfers/`
export const getAllTransactionsUriFrom = (safeAddress: string) => `/safes/${safeAddress}/all-transactions/`
export const getSafeCreationTxUri = (safeAddress: string) => `/safes/${safeAddress}/creation/`
export const getGoogleAnalyticsTrackingID = (): string => GOOGLE_ANALYTICS_ID
const fetchContractABI = memoize(
async (url: string, contractAddress: string, apiKey?: string) => {
let params: Record<string, string> = {
module: 'contract',
action: 'getAbi',
address: contractAddress,
} }
return process.env.REACT_APP_ENV === 'production' if (apiKey) {
? prodConfig params = { ...params, apiKey }
: stagingConfig }
const response = await fetch(`${url}?${new URLSearchParams(params)}`)
if (!response.ok) {
return { status: 0, result: [] }
}
return response.json()
},
(url, contractAddress) => `${url}_${contractAddress}`,
)
const getNetworkExplorerApiKey = (networkExplorerName: string): string | undefined=> {
switch (networkExplorerName.toLowerCase()) {
case 'etherscan': {
return ETHERSCAN_API_KEY
}
default: {
return undefined
}
} }
return process.env.REACT_APP_NETWORK === 'mainnet'
? mainnetDevConfig
: devConfig
} }
export const getNetwork = () => export const getContractABI = async (contractAddress: string) =>{
process.env.REACT_APP_NETWORK === 'mainnet' const { apiUrl, name } = getNetworkExplorerInfo()
? ETHEREUM_NETWORK.MAINNET
: ETHEREUM_NETWORK.RINKEBY
export const getNetworkId = () => const apiKey = getNetworkExplorerApiKey(name)
process.env.REACT_APP_NETWORK === 'mainnet' ? 1 : 4
const getConfig = ensureOnce(configuration) try {
const { result, status } = await fetchContractABI(apiUrl, contractAddress, apiKey)
export const getTxServiceHost = () => { if (status === '0') {
const config = getConfig() return []
}
return config[TX_SERVICE_HOST] return result
} catch (e) {
console.error('Failed to retrieve ABI', e)
return undefined
}
} }
export const getTxServiceUriFrom = (safeAddress) => export type BlockScanInfo = () => {
`safes/${safeAddress}/transactions/` alt: string
url: string
export const getIncomingTxServiceUriTo = (safeAddress) =>
`safes/${safeAddress}/incoming-transfers/`
export const getAllTransactionsUriFrom = (safeAddress: string): string =>
`safes/${safeAddress}/all-transactions/`
export const getSafeCreationTxUri = (safeAddress) => `safes/${safeAddress}/creation/`
export const getRelayUrl = () => getConfig()[RELAY_API_URL]
export const signaturesViaMetamask = () => {
const config = getConfig()
return config[SIGNATURES_VIA_METAMASK]
} }
export const getGnosisSafeAppsUrl = () => { export const getExplorerInfo = (hash: string): BlockScanInfo => {
const config = getConfig() const { name, url } = getNetworkExplorerInfo()
const networkInfo = getNetworkInfo()
return config[SAFE_APPS_URL] switch (networkInfo.id) {
} default: {
const type = hash.length > 42 ? 'tx' : 'address'
export const getGoogleAnalyticsTrackingID = () => return () => ({
getNetwork() === ETHEREUM_NETWORK.MAINNET url: `${url}${type}/${hash}`,
? process.env.REACT_APP_GOOGLE_ANALYTICS_ID_MAINNET alt: name || '',
: process.env.REACT_APP_GOOGLE_ANALYTICS_ID_RINKEBY })
}
export const getIntercomId = () => }
process.env.REACT_APP_ENV === 'production'
? process.env.REACT_APP_INTERCOM_ID
: 'plssl1fl'
export const getExchangeRatesUrl = () => 'https://api.exchangeratesapi.io/latest'
export const getExchangeRatesUrlFallback = () => 'https://api.coinbase.com/v2/exchange-rates'
export const getSafeLastVersion = () => process.env.REACT_APP_LATEST_SAFE_VERSION || '1.1.1'
export const buildSafeCreationTxUrl = (safeAddress) => {
const host = getTxServiceHost()
const address = checksumAddress(safeAddress)
const base = getSafeCreationTxUri(address)
return `${host}${base}`
} }

View File

@ -1,4 +0,0 @@
export const TX_SERVICE_HOST = 'tsh'
export const SIGNATURES_VIA_METAMASK = 'svm'
export const RELAY_API_URL = 'rau'
export const SAFE_APPS_URL = 'sau'

View File

@ -0,0 +1,142 @@
import fs from 'fs'
import networks from 'src/config/networks'
import { ETHEREUM_NETWORK } from 'src/config/networks/network.d'
import { isValidURL } from 'src/utils/url'
describe('Networks config files test', () => {
const environments = ['dev', 'staging', 'production']
const NETWORKS_PATH = 'src/config/networks/'
const configFiles = fs.readdirSync(NETWORKS_PATH)
const networksFileNames = configFiles
.filter((file) => !fs.lstatSync(`${NETWORKS_PATH}${file}`).isDirectory())
.filter((file) => {
const [fileName, extension] = file.split('.')
return extension === 'ts' && fileName !== 'index'
})
.map((file) => file.split('.')[0])
it(`should verify that the network file is exported in the networks/index.ts file`, () => {
networksFileNames.forEach((networkFileName) => {
const isValid = !!networks[networkFileName]
if (!isValid) {
console.log(`Network file "${networkFileName}" is not exported in "networks/index.ts"`)
}
expect(isValid).toBeTruthy()
})
})
environments.forEach((environment) => {
networksFileNames.forEach((networkFileName) => {
it(`should validate "${environment}" environment URIs for ${networkFileName} config`, () => {
// Given
const networkConfig = networks[networkFileName]
// When
const networkConfigElement = networkConfig.environment[environment]
if (!networkConfigElement) {
return
}
const environmentConfigKeys = Object
.keys(networkConfigElement)
.filter((environmentConfigKey) =>
environmentConfigKey.endsWith('Uri') && !!networkConfigElement[environmentConfigKey]
)
// Then
environmentConfigKeys.forEach((environmentConfigKey) => {
const networkConfigElementUri = networkConfigElement[environmentConfigKey]
const isValid = isValidURL(networkConfigElementUri)
if (!isValid) {
console.log(`Invalid URI in "${networkFileName}" at ${environment}.${environmentConfigKey}:`, networkConfigElementUri)
}
expect(isValid).toBeTruthy()
})
})
})
})
networksFileNames.forEach((networkFileName) => {
it(`should have a valid 'decimal' value for 'nativeToken'`, () => {
// Given
const networkConfig = networks[networkFileName]
// When
const { decimals } = networkConfig.network.nativeCoin
// Then
const isValid = Number.isInteger(decimals) && decimals >= 0
if (!isValid) {
console.log(`Invalid value in "${networkFileName}" at network.decimals:`, decimals)
}
expect(isValid).toBeTruthy()
})
})
networksFileNames.forEach((networkFileName) => {
it(`should have one of 'ETHEREUM_NETWORK' values for 'network.id'`, () => {
// Given
const networkConfig = networks[networkFileName]
// When
const { id } = networkConfig.network
// Then
const isValid = ETHEREUM_NETWORK[id]
if (!isValid) {
console.log(`Invalid value in "${networkFileName}" at network.id:`, id)
}
expect(isValid).toBeTruthy()
})
})
networksFileNames.forEach((networkFileName) => {
it(`should have a valid CSS color defined for 'network.backgroundColor'`, () => {
// Given
const networkConfig = networks[networkFileName]
// When
const { backgroundColor } = networkConfig.network
// Then
const s = new Option().style
s.color = backgroundColor
const isValid = s.color !== ''
if (!isValid) {
console.log(`Invalid value in "${networkFileName}" at network.backgroundColor:`, backgroundColor)
}
expect(isValid).toBeTruthy()
})
it(`should have a valid CSS color defined for 'network.textColor'`, () => {
// Given
const networkConfig = networks[networkFileName]
// When
const { textColor } = networkConfig.network
// Then
const s = new Option().style
s.color = textColor
const isValid = s.color !== ''
if (!isValid) {
console.log(`Invalid value in "${networkFileName}" at network.textColor:`, textColor)
}
expect(isValid).toBeTruthy()
})
})
})

View File

@ -0,0 +1,11 @@
import local from './local'
import mainnet from './mainnet'
import rinkeby from './rinkeby'
import xdai from './xdai'
export default {
local,
mainnet,
rinkeby,
xdai,
}

View File

@ -0,0 +1,37 @@
import EtherLogo from 'src/assets/icons/icon_etherTokens.svg'
import { EnvironmentSettings, ETHEREUM_NETWORK, NetworkConfig } from 'src/config/networks/network.d'
const baseConfig: EnvironmentSettings = {
txServiceUrl: 'http://localhost:8000/api/v1',
relayApiUrl: 'https://safe-relay.staging.gnosisdev.com/api/v1',
safeAppsUrl: 'http://localhost:3002',
gasPriceOracleUrl: 'https://ethgasstation.info/json/ethgasAPI.json',
rpcServiceUrl: 'http://localhost:4447',
networkExplorerName: 'Etherscan',
networkExplorerUrl: 'https://rinkeby.etherscan.io',
networkExplorerApiUrl: 'https://api-rinkeby.etherscan.io/api',
}
const local: NetworkConfig = {
environment: {
production: {
...baseConfig,
},
},
network: {
id: ETHEREUM_NETWORK.LOCAL,
backgroundColor: '#E8673C',
textColor: '#ffffff',
label: 'LocalRPC',
isTestNet: true,
nativeCoin: {
address: '0x000',
name: 'Ether',
symbol: 'ETH',
decimals: 18,
logoUri: EtherLogo,
},
},
}
export default local

View File

@ -0,0 +1,45 @@
import EtherLogo from 'src/assets/icons/icon_etherTokens.svg'
import { EnvironmentSettings, ETHEREUM_NETWORK, NetworkConfig } from 'src/config/networks/network.d'
const baseConfig: EnvironmentSettings = {
txServiceUrl: 'https://safe-transaction.mainnet.staging.gnosisdev.com/api/v1',
safeAppsUrl: 'https://safe-apps.dev.gnosisdev.com',
gasPriceOracleUrl: 'https://ethgasstation.info/json/ethgasAPI.json',
rpcServiceUrl: 'https://mainnet.infura.io:443/v3',
networkExplorerName: 'Etherscan',
networkExplorerUrl: 'https://etherscan.io',
networkExplorerApiUrl: 'https://api.etherscan.io/api',
}
const mainnet: NetworkConfig = {
environment: {
dev: {
...baseConfig,
},
staging: {
...baseConfig,
safeAppsUrl: 'https://safe-apps.staging.gnosisdev.com',
},
production: {
...baseConfig,
txServiceUrl: 'https://safe-transaction.mainnet.gnosis.io/api/v1',
safeAppsUrl: 'https://apps.gnosis-safe.io',
},
},
network: {
id: ETHEREUM_NETWORK.MAINNET,
backgroundColor: '#E8E7E6',
textColor: '#001428',
label: 'Mainnet',
isTestNet: false,
nativeCoin: {
address: '0x000',
name: 'Ether',
symbol: 'ETH',
decimals: 18,
logoUri: EtherLogo,
},
}
}
export default mainnet

77
src/config/networks/network.d.ts vendored Normal file
View File

@ -0,0 +1,77 @@
// matches src/logic/tokens/store/model/token.ts `TokenProps` type
export enum FEATURES {
ERC721 = 'ERC721',
ERC1155 = 'ERC1155',
SAFE_APPS = 'SAFE_APPS',
CONTRACT_INTERACTION = 'CONTRACT_INTERACTION'
}
type Token = {
address: string
name: string
symbol: string
decimals: number
logoUri?: string
}
export enum ETHEREUM_NETWORK {
MAINNET = 1,
MORDEN = 2,
ROPSTEN = 3,
RINKEBY = 4,
GOERLI = 5,
KOVAN = 42,
XDAI = 100,
ENERGY_WEB_CHAIN = 246,
VOLTA = 73799,
UNKNOWN = 0,
LOCAL = 4447,
}
export type NetworkSettings = {
// TODO: id now seems to be unnecessary
id: ETHEREUM_NETWORK,
backgroundColor: string,
textColor: string,
label: string,
isTestNet: boolean,
nativeCoin: Token,
}
// something around this to display or not some critical sections in the app, depending on the network support
// I listed the ones that may conflict with the network.
// If non is present, all the sections are available.
export type SafeFeatures = FEATURES[]
type GasPrice = {
gasPrice: number
gasPriceOracleUrl?: string
} | {
gasPrice?: number
// for infura there's a REST API Token required stored in: `REACT_APP_INFURA_TOKEN`
gasPriceOracleUrl: string
}
export type EnvironmentSettings = GasPrice & {
txServiceUrl: string
// Shall we keep a reference to the relay?
relayApiUrl?: string
safeAppsUrl: string
rpcServiceUrl: string
networkExplorerName: string
networkExplorerUrl: string
networkExplorerApiUrl: string
}
type SafeEnvironments = {
dev?: EnvironmentSettings
staging?: EnvironmentSettings
production: EnvironmentSettings
}
export interface NetworkConfig {
network: NetworkSettings
disabledFeatures?: SafeFeatures
environment: SafeEnvironments
}

View File

@ -0,0 +1,45 @@
import EtherLogo from 'src/assets/icons/icon_etherTokens.svg'
import { EnvironmentSettings, ETHEREUM_NETWORK, NetworkConfig } from 'src/config/networks/network.d'
const baseConfig: EnvironmentSettings = {
txServiceUrl: 'https://safe-transaction.staging.gnosisdev.com/api/v1',
safeAppsUrl: 'https://safe-apps.dev.gnosisdev.com',
gasPriceOracleUrl: 'https://ethgasstation.info/json/ethgasAPI.json',
rpcServiceUrl: 'https://rinkeby.infura.io:443/v3',
networkExplorerName: 'Etherscan',
networkExplorerUrl: 'https://rinkeby.etherscan.io',
networkExplorerApiUrl: 'https://api-rinkeby.etherscan.io/api',
}
const rinkeby: NetworkConfig = {
environment: {
dev: {
...baseConfig,
},
staging: {
...baseConfig,
safeAppsUrl: 'https://safe-apps.staging.gnosisdev.com',
},
production: {
...baseConfig,
txServiceUrl: 'https://safe-transaction.rinkeby.gnosis.io/api/v1',
safeAppsUrl: 'https://apps.gnosis-safe.io',
},
},
network: {
id: ETHEREUM_NETWORK.RINKEBY,
backgroundColor: '#E8673C',
textColor: '#ffffff',
label: 'Rinkeby',
isTestNet: true,
nativeCoin: {
address: '0x000',
name: 'Ether',
symbol: 'ETH',
decimals: 18,
logoUri: EtherLogo,
},
},
}
export default rinkeby

View File

@ -0,0 +1,40 @@
import { EnvironmentSettings, ETHEREUM_NETWORK, NetworkConfig } from 'src/config/networks/network.d'
const baseConfig: EnvironmentSettings = {
txServiceUrl: 'https://safe-transaction.xdai.gnosis.io/api/v1',
safeAppsUrl: 'https://safe-apps-xdai.staging.gnosisdev.com',
gasPrice: 1e9,
rpcServiceUrl: 'https://dai.poa.network/',
networkExplorerName: 'Blockscout',
networkExplorerUrl: 'https://blockscout.com/poa/xdai',
networkExplorerApiUrl: 'https://blockscout.com/poa/xdai/api',
}
const xDai: NetworkConfig = {
environment: {
staging: {
...baseConfig
},
production: {
...baseConfig,
safeAppsUrl: 'https://apps-xdai.gnosis-safe.io',
},
},
network: {
id: ETHEREUM_NETWORK.XDAI,
backgroundColor: '#48A8A6',
textColor: '#ffffff',
label: 'xDai',
isTestNet: false,
nativeCoin: {
address: '0x000',
name: 'xDai',
symbol: 'xDai',
decimals: 18,
logoUri: '',
},
}
}
export default xDai

View File

@ -1,11 +0,0 @@
//
import prodConfig from './production'
import { TX_SERVICE_HOST, RELAY_API_URL } from 'src/config/names'
const prodMainnetConfig = {
...prodConfig,
[TX_SERVICE_HOST]: 'https://safe-transaction.mainnet.gnosis.io/api/v1/',
[RELAY_API_URL]: 'https://safe-relay.gnosis.io/api/v1/',
}
export default prodMainnetConfig

View File

@ -1,10 +0,0 @@
import { TX_SERVICE_HOST, SIGNATURES_VIA_METAMASK, RELAY_API_URL, SAFE_APPS_URL } from 'src/config/names'
const prodConfig = {
[TX_SERVICE_HOST]: 'https://safe-transaction.rinkeby.gnosis.io/api/v1/',
[SIGNATURES_VIA_METAMASK]: false,
[RELAY_API_URL]: 'https://safe-relay.rinkeby.gnosis.io/api/v1/',
[SAFE_APPS_URL]: 'https://apps.gnosis-safe.io/'
}
export default prodConfig

View File

@ -1,11 +0,0 @@
//
import stagingConfig from './staging'
import { TX_SERVICE_HOST, RELAY_API_URL } from 'src/config/names'
const stagingMainnetConfig = {
...stagingConfig,
[TX_SERVICE_HOST]: 'https://safe-transaction.mainnet.staging.gnosisdev.com/api/v1/',
[RELAY_API_URL]: 'https://safe-relay.mainnet.staging.gnosisdev.com/api/v1/',
}
export default stagingMainnetConfig

View File

@ -1,10 +0,0 @@
import { TX_SERVICE_HOST, SIGNATURES_VIA_METAMASK, RELAY_API_URL, SAFE_APPS_URL } from 'src/config/names'
const stagingConfig = {
[TX_SERVICE_HOST]: 'https://safe-transaction.staging.gnosisdev.com/api/v1/',
[SIGNATURES_VIA_METAMASK]: false,
[RELAY_API_URL]: 'https://safe-relay.staging.gnosisdev.com/api/v1/',
[SAFE_APPS_URL]: 'https://safe-apps.staging.gnosisdev.com'
}
export default stagingConfig

View File

@ -1,10 +0,0 @@
import { TX_SERVICE_HOST, SIGNATURES_VIA_METAMASK, RELAY_API_URL, SAFE_APPS_URL } from 'src/config/names'
const testConfig = {
[TX_SERVICE_HOST]: 'http://localhost:8000/api/v1/',
[SIGNATURES_VIA_METAMASK]: false,
[RELAY_API_URL]: 'https://safe-relay.staging.gnosisdev.com/api/v1',
[SAFE_APPS_URL]: 'http://localhost:3002/'
}
export default testConfig

View File

@ -7,6 +7,9 @@ import { saveAddressBook } from 'src/logic/addressBook/utils'
import { enhanceSnackbarForAction, getNotificationsFromTxType } from 'src/logic/notifications' import { enhanceSnackbarForAction, getNotificationsFromTxType } from 'src/logic/notifications'
import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar' import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar'
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions' import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
import { safesListSelector } from 'src/logic/safe/store/selectors'
import { sameAddress } from 'src/logic/wallets/ethAddresses'
import updateSafe from 'src/logic/safe/store/actions/updateSafe'
const watchedActions = [ADD_ENTRY, REMOVE_ENTRY, UPDATE_ENTRY, ADD_OR_UPDATE_ENTRY] const watchedActions = [ADD_ENTRY, REMOVE_ENTRY, UPDATE_ENTRY, ADD_OR_UPDATE_ENTRY]
@ -17,6 +20,7 @@ const addressBookMiddleware = (store) => (next) => async (action) => {
const state = store.getState() const state = store.getState()
const { dispatch } = store const { dispatch } = store
const addressBook = addressBookSelector(state) const addressBook = addressBookSelector(state)
const safes = safesListSelector(state)
if (addressBook.length) { if (addressBook.length) {
await saveAddressBook(addressBook) await saveAddressBook(addressBook)
} }
@ -36,8 +40,13 @@ const addressBookMiddleware = (store) => (next) => async (action) => {
break break
} }
case UPDATE_ENTRY: { case UPDATE_ENTRY: {
const { entry } = action.payload
const notification = getNotificationsFromTxType(TX_NOTIFICATION_TYPES.ADDRESSBOOK_EDIT_ENTRY) const notification = getNotificationsFromTxType(TX_NOTIFICATION_TYPES.ADDRESSBOOK_EDIT_ENTRY)
dispatch(enqueueSnackbar(enhanceSnackbarForAction(notification.afterExecution.noMoreConfirmationsNeeded))) dispatch(enqueueSnackbar(enhanceSnackbarForAction(notification.afterExecution.noMoreConfirmationsNeeded)))
const safeFound = safes.find((safe) => sameAddress(safe.address, entry.address))
if (safeFound) {
dispatch(updateSafe({ address: safeFound.address, name: entry.name }))
}
break break
} }
default: default:

View File

@ -29,9 +29,7 @@ export default handleActions(
const entryFound = state.find((oldEntry) => oldEntry.address === entry.address) const entryFound = state.find((oldEntry) => oldEntry.address === entry.address)
// Only adds entries with valid names if (!entryFound) {
const validName = getValidAddressBookName(entry.name)
if (!entryFound && validName) {
state.push(entry) state.push(entry)
} }
return state return state

View File

@ -0,0 +1,106 @@
import { RateLimit } from 'async-sema'
import { Collectibles, NFTAsset, NFTAssets, NFTTokens } from 'src/logic/collectibles/sources/collectibles.d'
import NFTIcon from 'src/routes/safe/components/Balances/assets/nft_icon.png'
import { fetchErc20AndErc721AssetsList, fetchSafeCollectibles } from 'src/logic/tokens/api'
import { TokenResult } from 'src/logic/tokens/api/fetchErc20AndErc721AssetsList'
import { CollectibleResult } from 'src/logic/tokens/api/fetchSafeCollectibles'
type FetchResult = {
erc721Assets: TokenResult[]
erc721Tokens: CollectibleResult[]
}
class Gnosis {
_rateLimit = async (): Promise<void> => {}
_fetch = async (safeAddress: string): Promise<FetchResult> => {
const collectibles: FetchResult = {
erc721Assets: [],
erc721Tokens: [],
}
try {
const {
data: { results: assets = [] },
} = await fetchErc20AndErc721AssetsList()
collectibles.erc721Assets = assets.filter((token) => token.type.toLowerCase() === 'erc721')
} catch (e) {
console.error('no erc721 assets could be fetched', e)
}
try {
const { data: tokens = [] } = await fetchSafeCollectibles(safeAddress)
collectibles.erc721Tokens = tokens
} catch (e) {
console.error('no erc721 tokens for the current safe', e)
}
return collectibles
}
/**
* OpenSea class constructor
* @param {object} options
* @param {number} options.rps - requests per second
*/
constructor(options: { rps: number }) {
// eslint-disable-next-line no-underscore-dangle
this._rateLimit = RateLimit(options.rps, { timeUnit: 60 * 1000, uniformDistribution: true })
}
static extractAssets(assets: TokenResult[], nftTokens: NFTTokens): NFTAssets {
const extractNFTAsset = (asset: TokenResult): NFTAsset => {
const numberOfTokens = nftTokens.filter(({ assetAddress }) => assetAddress === asset.address).length
return {
address: asset.address,
description: asset.name,
image: asset.logoUri || NFTIcon,
name: asset.name,
numberOfTokens,
slug: `${asset.address}_${asset.name}`,
symbol: asset.symbol,
}
}
return assets.reduce((acc, asset) => {
const address = asset.address
if (acc[address] === undefined) {
acc[address] = extractNFTAsset(asset)
}
return acc
}, {})
}
static extractTokens(tokens: CollectibleResult[]): NFTTokens {
return tokens.map((token) => ({
assetAddress: token.address,
color: 'red',
description: token.description || '',
image: token.imageUri || NFTIcon,
name: token.name || '',
tokenId: token.id,
}))
}
/**
* Fetches from OpenSea the list of collectibles, grouped by category,
* for the provided Safe Address in the specified Network
* @param {string} safeAddress
* @returns {Promise<Collectibles>}
*/
async fetchCollectibles(safeAddress: string): Promise<Collectibles> {
const { erc721Assets, erc721Tokens } = await this._fetch(safeAddress)
const nftTokens = Gnosis.extractTokens(erc721Tokens)
return {
nftTokens,
nftAssets: Gnosis.extractAssets(erc721Assets, nftTokens),
}
}
}
export default Gnosis

View File

@ -1,61 +1,11 @@
import { RateLimit } from 'async-sema' import { RateLimit } from 'async-sema'
import { getNetworkId } from 'src/config'
import { ETHEREUM_NETWORK } from 'src/logic/wallets/getWeb3' import { ETHEREUM_NETWORK } from 'src/config/networks/network.d'
import { Collectibles, NFTAssets, NFTTokens, OpenSeaAssets } from 'src/logic/collectibles/sources/collectibles.d'
import NFTIcon from 'src/routes/safe/components/Balances/assets/nft_icon.png' import NFTIcon from 'src/routes/safe/components/Balances/assets/nft_icon.png'
import { OPENSEA_API_KEY } from 'src/utils/constants' import { OPENSEA_API_KEY } from 'src/utils/constants'
export interface OpenSeaAssetContract {
address: string
name: string
image_url: string
symbol: string
}
export interface OpenSeaCollection {
name: string
slug: string
}
export interface OpenSeaAsset {
asset_contract: OpenSeaAssetContract
background_color: string
collection: OpenSeaCollection
description: string
image_thumbnail_url: string
name: string
token_id: string
}
export type OpenSeaAssets = Array<OpenSeaAsset>
export interface NFTAsset {
address: string
assetContract: OpenSeaAssetContract
collection: OpenSeaCollection
description: string
image: string
name: string
numberOfTokens: number
slug: string
symbol: string
}
export type NFTAssets = Record<string, NFTAsset>
export interface NFTToken {
assetAddress: string
color: string
description: string
image: string
name: string
tokenId: number | string
}
export type NFTTokens = Array<NFTToken>
export interface Collectibles {
nftAssets: NFTAssets
nftTokens: NFTTokens
}
class OpenSea { class OpenSea {
_rateLimit = async (): Promise<void> => {} _rateLimit = async (): Promise<void> => {}
@ -133,14 +83,11 @@ class OpenSea {
* Fetches from OpenSea the list of collectibles, grouped by category, * Fetches from OpenSea the list of collectibles, grouped by category,
* for the provided Safe Address in the specified Network * for the provided Safe Address in the specified Network
* @param {string} safeAddress * @param {string} safeAddress
* @param {string} network
* @returns {Promise<Collectibles>} * @returns {Promise<Collectibles>}
*/ */
async fetchAllUserCollectiblesByCategoryAsync(safeAddress: string, network: string): Promise<Collectibles> { async fetchCollectibles(safeAddress: string): Promise<Collectibles> {
// eslint-disable-next-line no-underscore-dangle const metadataSourceUrl = this._endpointsUrls[getNetworkId()]
const metadataSourceUrl = this._endpointsUrls[network]
const url = `${metadataSourceUrl}/assets/?owner=${safeAddress}` const url = `${metadataSourceUrl}/assets/?owner=${safeAddress}`
// eslint-disable-next-line no-underscore-dangle
const assetsResponse = await this._fetch(url) const assetsResponse = await this._fetch(url)
const assetsResponseJson = await assetsResponse.json() const assetsResponseJson = await assetsResponse.json()
return OpenSea.extractCollectiblesInfo(assetsResponseJson) return OpenSea.extractCollectiblesInfo(assetsResponseJson)

View File

@ -0,0 +1,53 @@
export interface OpenSeaAssetContract {
address: string
name: string
image_url: string
symbol: string
}
export interface OpenSeaCollection {
name: string
slug: string
}
export interface OpenSeaAsset {
asset_contract: OpenSeaAssetContract
background_color: string
collection: OpenSeaCollection
description: string
image_thumbnail_url: string
name: string
token_id: string
}
export type OpenSeaAssets = Array<OpenSeaAsset>
export interface NFTAsset {
address: string
assetContract?: OpenSeaAssetContract
collection?: OpenSeaCollection
description: string
image: string
name: string
numberOfTokens: number
slug: string
symbol: string
}
export type NFTAssets = Record<string, NFTAsset>
export interface NFTToken {
assetAddress: string
color: string
description: string
image: string
name: string
tokenId: number | string
}
export type NFTTokens = Array<NFTToken>
export interface Collectibles {
nftAssets: NFTAssets
nftTokens: NFTTokens
}

View File

@ -1,13 +1,15 @@
import MockedOpenSea from 'src/logic/collectibles/sources/MockedOpenSea' import MockedOpenSea from 'src/logic/collectibles/sources/MockedOpenSea'
import OpenSea from 'src/logic/collectibles/sources/OpenSea' import OpenSea from 'src/logic/collectibles/sources/OpenSea'
import Gnosis from 'src/logic/collectibles/sources/Gnosis'
import { COLLECTIBLES_SOURCE } from 'src/utils/constants' import { COLLECTIBLES_SOURCE } from 'src/utils/constants'
const SOURCES = { const SOURCES = {
opensea: new OpenSea({ rps: 4 }), opensea: new OpenSea({ rps: 4 }),
gnosis: new Gnosis({ rps: 4 }),
mockedopensea: new MockedOpenSea({ rps: 4 }), mockedopensea: new MockedOpenSea({ rps: 4 }),
} }
type Sources = typeof SOURCES type Sources = typeof SOURCES
export const getConfiguredSource = (): Sources['opensea'] | Sources['mockedopensea'] => export const getConfiguredSource = (): Sources['opensea'] | Sources['mockedopensea'] | Sources['gnosis'] =>
SOURCES[COLLECTIBLES_SOURCE.toLowerCase()] SOURCES[COLLECTIBLES_SOURCE.toLowerCase()]

View File

@ -1,15 +1,13 @@
import { batch } from 'react-redux' import { batch } from 'react-redux'
import { Dispatch } from 'redux'
import { getNetwork } from 'src/config'
import { getConfiguredSource } from 'src/logic/collectibles/sources' import { getConfiguredSource } from 'src/logic/collectibles/sources'
import { addNftAssets, addNftTokens } from 'src/logic/collectibles/store/actions/addCollectibles' import { addNftAssets, addNftTokens } from 'src/logic/collectibles/store/actions/addCollectibles'
import { Dispatch } from 'redux'
const fetchCollectibles = (safeAddress: string) => async (dispatch: Dispatch): Promise<void> => { const fetchCollectibles = (safeAddress: string) => async (dispatch: Dispatch): Promise<void> => {
try { try {
const network = getNetwork()
const source = getConfiguredSource() const source = getConfiguredSource()
const collectibles = await source.fetchAllUserCollectiblesByCategoryAsync(safeAddress, network) const collectibles = await source.fetchCollectibles(safeAddress)
batch(() => { batch(() => {
dispatch(addNftAssets(collectibles.nftAssets)) dispatch(addNftAssets(collectibles.nftAssets))

View File

@ -1,14 +1,18 @@
import { createSelector } from 'reselect' import { createSelector } from 'reselect'
import { NFTAsset, NFTAssets, NFTTokens } from 'src/logic/collectibles/sources/OpenSea' import { NFTAsset, NFTAssets, NFTTokens } from 'src/logic/collectibles/sources/collectibles.d'
import { AppReduxState } from 'src/store' import { AppReduxState } from 'src/store'
import { NFT_ASSETS_REDUCER_ID, NFT_TOKENS_REDUCER_ID } from 'src/logic/collectibles/store/reducer/collectibles' import { NFT_ASSETS_REDUCER_ID, NFT_TOKENS_REDUCER_ID } from 'src/logic/collectibles/store/reducer/collectibles'
import { safeActiveAssetsSelector } from 'src/logic/safe/store/selectors' import { safeActiveAssetsSelector } from 'src/logic/safe/store/selectors'
export const nftAssetsSelector = (state: AppReduxState): NFTAssets => state[NFT_ASSETS_REDUCER_ID] export const nftAssets = (state: AppReduxState): NFTAssets => state[NFT_ASSETS_REDUCER_ID]
export const nftTokensSelector = (state: AppReduxState): NFTTokens => state[NFT_TOKENS_REDUCER_ID] export const nftTokens = (state: AppReduxState): NFTTokens => state[NFT_TOKENS_REDUCER_ID]
export const nftAssetsListSelector = createSelector(nftAssetsSelector, (assets): NFTAsset[] => { export const nftAssetsSelector = createSelector(nftAssets, (assets) => assets)
export const nftTokensSelector = createSelector(nftTokens, (tokens) => tokens)
export const nftAssetsListSelector = createSelector(nftAssets, (assets): NFTAsset[] => {
return assets ? Object.values(assets) : [] return assets ? Object.values(assets) : []
}) })

View File

@ -1,60 +0,0 @@
import { RateLimit } from 'async-sema'
import memoize from 'lodash.memoize'
import { ETHEREUM_NETWORK } from 'src/logic/wallets/getWeb3'
import { ETHERSCAN_API_KEY } from 'src/utils/constants'
class EtherscanService {
_rateLimit = async () => {}
_endpointsUrls = {
[ETHEREUM_NETWORK.MAINNET]: 'https://api.etherscan.io/api',
[ETHEREUM_NETWORK.RINKEBY]: 'https://api-rinkeby.etherscan.io/api',
}
_fetch = memoize(
async (url: string, contractAddress: string) => {
let params: any = {
module: 'contract',
action: 'getAbi',
address: contractAddress,
}
if (ETHERSCAN_API_KEY) {
const apiKey = ETHERSCAN_API_KEY
params = { ...params, apiKey }
}
const response = await fetch(`${url}?${new URLSearchParams(params)}`)
if (!response.ok) {
return { status: 0, result: [] }
}
return response.json()
},
(url, contractAddress) => `${url}_${contractAddress}`,
)
constructor(options) {
this._rateLimit = RateLimit(options.rps)
}
async getContractABI(contractAddress, network) {
const etherscanUrl = this._endpointsUrls[network]
try {
const { result, status } = await this._fetch(etherscanUrl, contractAddress)
if (status === '0') {
return []
}
return result
} catch (e) {
console.error('Failed to retrieve ABI', e)
return undefined
}
}
}
export default EtherscanService

View File

@ -1,7 +0,0 @@
import EtherscanService from 'src/logic/contractInteraction/sources/EtherscanService'
const sources = {
etherscan: new EtherscanService({ rps: 4 }),
}
export const getConfiguredSource = () => sources['etherscan']

View File

@ -1,4 +1,6 @@
import { web3ReadOnly as web3 } from 'src/logic/wallets/getWeb3' import { web3ReadOnly as web3 } from 'src/logic/wallets/getWeb3'
import { BatchRequest } from 'web3-core'
import { AbiItem } from 'web3-utils'
/** /**
* Generates a batch request for grouping RPC calls * Generates a batch request for grouping RPC calls
@ -10,23 +12,33 @@ import { web3ReadOnly as web3 } from 'src/logic/wallets/getWeb3'
* @param {array<{ args: [any], method: string, type: 'eth'|undefined } | string>} args.methods - methods to be called * @param {array<{ args: [any], method: string, type: 'eth'|undefined } | string>} args.methods - methods to be called
* @returns {Promise<[*]>} * @returns {Promise<[*]>}
*/ */
const generateBatchRequests = ({ abi, address, batch, context, methods }: any): any => { type MethodsArgsType = Array<string | number>
const contractInstance: any = new web3.eth.Contract(abi, address)
interface Props {
abi: AbiItem[]
address: string
batch?: BatchRequest
context?: unknown
methods: Array<string | {method: string, type?: string, args: MethodsArgsType }>
}
const generateBatchRequests = <ReturnValues>({ abi, address, batch, context, methods }: Props): Promise<ReturnValues> => {
const contractInstance = new web3.eth.Contract(abi, address)
const localBatch = new web3.BatchRequest() const localBatch = new web3.BatchRequest()
const values = methods.map((methodObject) => { const values = methods.map((methodObject) => {
let method, type, args = [] let method, type, args: MethodsArgsType = []
if (typeof methodObject === 'string') { if (typeof methodObject === 'string') {
method = methodObject method = methodObject
} else { } else {
;({ method, type, args = [] } = methodObject) ({ method, type, args } = methodObject)
} }
return new Promise((resolve) => { return new Promise((resolve) => {
const resolver = (error, result) => { const resolver = (error, result) => {
if (error) { if (error) {
resolve(null) resolve()
} else { } else {
resolve(result) resolve(result)
} }
@ -43,7 +55,8 @@ const generateBatchRequests = ({ abi, address, batch, context, methods }: any):
// If batch was provided add to external batch // If batch was provided add to external batch
batch ? batch.add(request) : localBatch.add(request) batch ? batch.add(request) : localBatch.add(request)
} catch (e) { } catch (e) {
resolve(null) console.error('There was an error trying to batch request from web3.', e)
resolve()
} }
}) })
}) })
@ -54,9 +67,8 @@ const generateBatchRequests = ({ abi, address, batch, context, methods }: any):
// in the outside function where the batch object is created. // in the outside function where the batch object is created.
!batch && localBatch.execute() !batch && localBatch.execute()
const returnValues = context ? [context, ...values] : values // @ts-ignore
return Promise.all([context, ...values])
return Promise.all(returnValues)
} }
export default generateBatchRequests export default generateBatchRequests

View File

@ -1,17 +1,17 @@
import { AbiItem } from 'web3-utils' import { AbiItem } from 'web3-utils'
import contract from 'truffle-contract'
import Web3 from 'web3'
import ProxyFactorySol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafeProxyFactory.json'
import GnosisSafeSol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafe.json' import GnosisSafeSol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafe.json'
import SafeProxy from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafeProxy.json'
import { ensureOnce } from 'src/utils/singleton'
import memoize from 'lodash.memoize' import memoize from 'lodash.memoize'
import { getWeb3, getNetworkIdFrom } from 'src/logic/wallets/getWeb3' import ProxyFactorySol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafeProxyFactory.json'
import { calculateGasOf, calculateGasPrice } from 'src/logic/wallets/ethTransactions' import SafeProxy from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafeProxy.json'
import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses' import Web3 from 'web3'
import { ETHEREUM_NETWORK } from 'src/config/networks/network.d'
import { isProxyCode } from 'src/logic/contracts/historicProxyCode' import { isProxyCode } from 'src/logic/contracts/historicProxyCode'
import { GnosisSafeProxyFactory } from 'src/types/contracts/GnosisSafeProxyFactory.d'; import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses'
import { calculateGasOf, calculateGasPrice } from 'src/logic/wallets/ethTransactions'
import { getWeb3, getNetworkIdFrom } from 'src/logic/wallets/getWeb3'
import { GnosisSafe } from 'src/types/contracts/GnosisSafe.d' import { GnosisSafe } from 'src/types/contracts/GnosisSafe.d'
import { GnosisSafeProxyFactory } from 'src/types/contracts/GnosisSafeProxyFactory.d'
export const SENTINEL_ADDRESS = '0x0000000000000000000000000000000000000001' export const SENTINEL_ADDRESS = '0x0000000000000000000000000000000000000001'
export const MULTI_SEND_ADDRESS = '0x8d29be29923b68abfdd21e541b9374737b49cdad' export const MULTI_SEND_ADDRESS = '0x8d29be29923b68abfdd21e541b9374737b49cdad'
@ -20,24 +20,39 @@ export const DEFAULT_FALLBACK_HANDLER_ADDRESS = '0xd5D82B6aDDc9027B22dCA772Aa68D
export const SAFE_MASTER_COPY_ADDRESS_V10 = '0xb6029EA3B2c51D09a50B53CA8012FeEB05bDa35A' export const SAFE_MASTER_COPY_ADDRESS_V10 = '0xb6029EA3B2c51D09a50B53CA8012FeEB05bDa35A'
let proxyFactoryMaster let proxyFactoryMaster: GnosisSafeProxyFactory
let safeMaster let safeMaster: GnosisSafe
const createGnosisSafeContract = (web3: Web3) => { /**
const gnosisSafe = contract(GnosisSafeSol) * Creates a Contract instance of the GnosisSafe contract
gnosisSafe.setProvider(web3.currentProvider) * @param {Web3} web3
* @param {ETHEREUM_NETWORK} networkId
return gnosisSafe */
const createGnosisSafeContract = (web3: Web3, networkId: ETHEREUM_NETWORK) => {
const networks = GnosisSafeSol.networks
// TODO: this may not be the most scalable approach,
// but up until v1.2.0 the address is the same for all the networks.
// So, if we can't find the network in the Contract artifact, we fallback to MAINNET.
const contractAddress = networks[networkId]?.address ?? networks[ETHEREUM_NETWORK.MAINNET].address
return new web3.eth.Contract(GnosisSafeSol.abi as AbiItem[], contractAddress) as unknown as GnosisSafe
} }
const createProxyFactoryContract = (web3: Web3, networkId: number): GnosisSafeProxyFactory => { /**
const contractAddress = ProxyFactorySol.networks[networkId].address * Creates a Contract instance of the GnosisSafeProxyFactory contract
const proxyFactory = new web3.eth.Contract(ProxyFactorySol.abi as AbiItem[], contractAddress) as unknown as GnosisSafeProxyFactory * @param {Web3} web3
* @param {ETHEREUM_NETWORK} networkId
return proxyFactory */
const createProxyFactoryContract = (web3: Web3, networkId: ETHEREUM_NETWORK): GnosisSafeProxyFactory => {
const networks = ProxyFactorySol.networks
// TODO: this may not be the most scalable approach,
// but up until v1.2.0 the address is the same for all the networks.
// So, if we can't find the network in the Contract artifact, we fallback to MAINNET.
const contractAddress = networks[networkId]?.address ?? networks[ETHEREUM_NETWORK.MAINNET].address
return new web3.eth.Contract(ProxyFactorySol.abi as AbiItem[], contractAddress) as unknown as GnosisSafeProxyFactory
} }
export const getGnosisSafeContract = memoize(createGnosisSafeContract) export const getGnosisSafeContract = memoize(createGnosisSafeContract)
const getCreateProxyFactoryContract = memoize(createProxyFactoryContract) const getCreateProxyFactoryContract = memoize(createProxyFactoryContract)
const instantiateMasterCopies = async () => { const instantiateMasterCopies = async () => {
@ -47,25 +62,11 @@ const instantiateMasterCopies = async () => {
// Create ProxyFactory Master Copy // Create ProxyFactory Master Copy
proxyFactoryMaster = getCreateProxyFactoryContract(web3, networkId) proxyFactoryMaster = getCreateProxyFactoryContract(web3, networkId)
// Initialize Safe master copy // Create Safe Master copy
const GnosisSafe = getGnosisSafeContract(web3) safeMaster = getGnosisSafeContract(web3, networkId)
safeMaster = await GnosisSafe.deployed()
} }
// ONLY USED IN TEST ENVIRONMENT export const initContracts = instantiateMasterCopies
const createMasterCopies = async () => {
const web3 = getWeb3()
const accounts = await web3.eth.getAccounts()
const userAccount = accounts[0]
const ProxyFactory = getCreateProxyFactoryContract(web3, 4441)
proxyFactoryMaster = await ProxyFactory.deploy({ data: GnosisSafeSol.bytecode }).send({ from: userAccount, gas: 5000000 })
const GnosisSafe = getGnosisSafeContract(web3)
safeMaster = await GnosisSafe.new({ from: userAccount, gas: '7000000' })
}
export const initContracts = process.env.NODE_ENV === 'test' ? ensureOnce(createMasterCopies) : instantiateMasterCopies
export const getSafeMasterContract = async () => { export const getSafeMasterContract = async () => {
await initContracts() await initContracts()
@ -74,11 +75,11 @@ export const getSafeMasterContract = async () => {
} }
export const getSafeDeploymentTransaction = (safeAccounts, numConfirmations) => { export const getSafeDeploymentTransaction = (safeAccounts, numConfirmations) => {
const gnosisSafeData = safeMaster.contract.methods const gnosisSafeData = safeMaster.methods
.setup(safeAccounts, numConfirmations, ZERO_ADDRESS, '0x', DEFAULT_FALLBACK_HANDLER_ADDRESS, ZERO_ADDRESS, 0, ZERO_ADDRESS) .setup(safeAccounts, numConfirmations, ZERO_ADDRESS, '0x', DEFAULT_FALLBACK_HANDLER_ADDRESS, ZERO_ADDRESS, 0, ZERO_ADDRESS)
.encodeABI() .encodeABI()
return proxyFactoryMaster.methods.createProxy(safeMaster.address, gnosisSafeData) return proxyFactoryMaster.methods.createProxy(safeMaster.options.address, gnosisSafeData)
} }
export const estimateGasForDeployingSafe = async ( export const estimateGasForDeployingSafe = async (
@ -86,13 +87,13 @@ export const estimateGasForDeployingSafe = async (
numConfirmations, numConfirmations,
userAccount, userAccount,
) => { ) => {
const gnosisSafeData = await safeMaster.contract.methods const gnosisSafeData = await safeMaster.methods
.setup(safeAccounts, numConfirmations, ZERO_ADDRESS, '0x', DEFAULT_FALLBACK_HANDLER_ADDRESS, ZERO_ADDRESS, 0, ZERO_ADDRESS) .setup(safeAccounts, numConfirmations, ZERO_ADDRESS, '0x', DEFAULT_FALLBACK_HANDLER_ADDRESS, ZERO_ADDRESS, 0, ZERO_ADDRESS)
.encodeABI() .encodeABI()
const proxyFactoryData = proxyFactoryMaster.methods const proxyFactoryData = proxyFactoryMaster.methods
.createProxy(safeMaster.address, gnosisSafeData) .createProxy(safeMaster.options.address, gnosisSafeData)
.encodeABI() .encodeABI()
const gas = await calculateGasOf(proxyFactoryData, userAccount, proxyFactoryMaster.address) const gas = await calculateGasOf(proxyFactoryData, userAccount, proxyFactoryMaster.options.address)
const gasPrice = await calculateGasPrice() const gasPrice = await calculateGasPrice()
return gas * parseInt(gasPrice, 10) return gas * parseInt(gasPrice, 10)

View File

@ -1,8 +1,8 @@
import Cookies from 'js-cookie' import Cookies from 'js-cookie'
import { getNetwork } from 'src/config' import { getNetworkName } from 'src/config'
const PREFIX = `v1_${getNetwork()}` const PREFIX = `v1_${getNetworkName()}`
export const loadFromCookie = async (key) => { export const loadFromCookie = async (key) => {
try { try {

View File

@ -1,7 +1,7 @@
import { aNewStore } from 'src/store' import { aNewStore } from 'src/store'
import fetchTokenCurrenciesBalances from 'src/logic/currencyValues/api/fetchTokenCurrenciesBalances' import { fetchTokenCurrenciesBalances } from 'src/logic/currencyValues/api/fetchTokenCurrenciesBalances'
import axios from 'axios' import axios from 'axios'
import { getTxServiceHost } from 'src/config' import { getTxServiceUrl } from 'src/config'
jest.mock('axios') jest.mock('axios')
describe('fetchTokenCurrenciesBalances', () => { describe('fetchTokenCurrenciesBalances', () => {
@ -19,26 +19,28 @@ describe('fetchTokenCurrenciesBalances', () => {
// given // given
const expectedResult = [ const expectedResult = [
{ {
balance: '849890000000000000',
balanceUsd: '337.2449',
token: null,
tokenAddress: null, tokenAddress: null,
usdConversion: '396.81', token: null,
balance: '849890000000000000',
fiatBalance: '337.2449',
fiatConversion: '396.81',
fiatCode: 'USD',
}, },
{ {
balance: '24698677800000000000', tokenAddress: '0x5592EC0cfb4dbc12D3aB100b257153436a1f0FEa',
balanceUsd: '29.3432',
token: { token: {
name: 'Dai', name: 'Dai',
symbol: 'DAI', symbol: 'DAI',
decimals: 18, decimals: 18,
logoUri: 'https://gnosis-safe-token-logos.s3.amazonaws.com/0x5592EC0cfb4dbc12D3aB100b257153436a1f0FEa.png', logoUri: 'https://gnosis-safe-token-logos.s3.amazonaws.com/0x5592EC0cfb4dbc12D3aB100b257153436a1f0FEa.png',
}, },
tokenAddress: '0x5592EC0cfb4dbc12D3aB100b257153436a1f0FEa', balance: '24698677800000000000',
usdConversion: '1.188', fiatBalance: '29.3432',
fiatConversion: '1.188',
fiatCode: 'USD',
}, },
] ]
const apiUrl = getTxServiceHost() const apiUrl = getTxServiceUrl()
// @ts-ignore // @ts-ignore
axios.get.mockImplementationOnce(() => Promise.resolve(expectedResult)) axios.get.mockImplementationOnce(() => Promise.resolve(expectedResult))
@ -49,8 +51,6 @@ describe('fetchTokenCurrenciesBalances', () => {
// then // then
expect(result).toStrictEqual(expectedResult) expect(result).toStrictEqual(expectedResult)
expect(axios.get).toHaveBeenCalled() expect(axios.get).toHaveBeenCalled()
expect(axios.get).toBeCalledWith(`${apiUrl}safes/${safeAddress}/balances/usd/?exclude_spam=${excludeSpamTokens}`, { expect(axios.get).toBeCalledWith(`${apiUrl}/safes/${safeAddress}/balances/usd/?exclude_spam=${excludeSpamTokens}`)
params: { limit: 3000 },
})
}) })
}) })

View File

@ -1,8 +1,8 @@
import axios from 'axios' import axios from 'axios'
import { getExchangeRatesUrl } from 'src/config' import { EXCHANGE_RATE_URL } from 'src/utils/constants'
import { AVAILABLE_CURRENCIES } from '../store/model/currencyValues' import { AVAILABLE_CURRENCIES } from '../store/model/currencyValues'
import fetchTokenCurrenciesBalances from './fetchTokenCurrenciesBalances' import { fetchTokenCurrenciesBalances } from './fetchTokenCurrenciesBalances'
import BigNumber from 'bignumber.js' import BigNumber from 'bignumber.js'
const fetchCurrenciesRates = async ( const fetchCurrenciesRates = async (
@ -16,7 +16,7 @@ const fetchCurrenciesRates = async (
try { try {
const result = await fetchTokenCurrenciesBalances(safeAddress) const result = await fetchTokenCurrenciesBalances(safeAddress)
if (result?.data?.length) { if (result?.data?.length) {
rate = new BigNumber(1).div(result.data[0].usdConversion).toNumber() rate = new BigNumber(1).div(result.data[0].fiatConversion).toNumber()
} }
} catch (error) { } catch (error) {
console.error('Fetching ETH data from the relayer errored', error) console.error('Fetching ETH data from the relayer errored', error)
@ -25,7 +25,7 @@ const fetchCurrenciesRates = async (
} }
try { try {
const url = `${getExchangeRatesUrl()}?base=${baseCurrency}&symbols=${targetCurrencyValue}` const url = `${EXCHANGE_RATE_URL}?base=${baseCurrency}&symbols=${targetCurrencyValue}`
const result = await axios.get(url) const result = await axios.get(url)
if (result?.data) { if (result?.data) {
const { rates } = result.data const { rates } = result.data

View File

@ -1,28 +1,24 @@
import axios, { AxiosResponse } from 'axios' import axios, { AxiosResponse } from 'axios'
import { getTxServiceHost } from 'src/config' import { getTxServiceUrl } from 'src/config'
import { TokenProps } from 'src/logic/tokens/store/model/token' import { TokenProps } from 'src/logic/tokens/store/model/token'
import { AVAILABLE_CURRENCIES } from '../store/model/currencyValues'
export type BalanceEndpoint = { export type BalanceEndpoint = {
balance: string
balanceUsd: string
tokenAddress: string tokenAddress: string
token?: TokenProps token?: TokenProps
usdConversion: string balance: string
fiatBalance: string
fiatConversion: string
fiatCode: AVAILABLE_CURRENCIES
} }
const fetchTokenCurrenciesBalances = ( export const fetchTokenCurrenciesBalances = (
safeAddress: string, safeAddress: string,
excludeSpamTokens = true, excludeSpamTokens = true,
): Promise<AxiosResponse<BalanceEndpoint[]>> => { ): Promise<AxiosResponse<BalanceEndpoint[]>> => {
const apiUrl = getTxServiceHost() const apiUrl = getTxServiceUrl()
const url = `${apiUrl}safes/${safeAddress}/balances/usd/?exclude_spam=${excludeSpamTokens}` const url = `${apiUrl}/safes/${safeAddress}/balances/usd/?exclude_spam=${excludeSpamTokens}`
return axios.get(url, { return axios.get(url)
params: {
limit: 3000,
},
})
} }
export default fetchTokenCurrenciesBalances

View File

@ -1,7 +1,6 @@
import { OptionsObject } from 'notistack' import { OptionsObject } from 'notistack'
import { getNetwork } from 'src/config' import { getNetworkName } from 'src/config'
import { capitalize } from 'src/utils/css'
export const SUCCESS = 'success' export const SUCCESS = 'success'
export const ERROR = 'error' export const ERROR = 'error'
@ -46,7 +45,7 @@ const NOTIFICATION_IDS = {
SETTINGS_CHANGE_EXECUTED_MSG: 'SETTINGS_CHANGE_EXECUTED_MSG', SETTINGS_CHANGE_EXECUTED_MSG: 'SETTINGS_CHANGE_EXECUTED_MSG',
SETTINGS_CHANGE_EXECUTED_MORE_CONFIRMATIONS_MSG: 'SETTINGS_CHANGE_EXECUTED_MORE_CONFIRMATIONS_MSG', SETTINGS_CHANGE_EXECUTED_MORE_CONFIRMATIONS_MSG: 'SETTINGS_CHANGE_EXECUTED_MORE_CONFIRMATIONS_MSG',
SETTINGS_CHANGE_FAILED_MSG: 'SETTINGS_CHANGE_FAILED_MSG', SETTINGS_CHANGE_FAILED_MSG: 'SETTINGS_CHANGE_FAILED_MSG',
RINKEBY_VERSION_MSG: 'RINKEBY_VERSION_MSG', TESTNET_VERSION_MSG: 'TESTNET_VERSION_MSG',
WRONG_NETWORK_MSG: 'WRONG_NETWORK_MSG', WRONG_NETWORK_MSG: 'WRONG_NETWORK_MSG',
ADDRESS_BOOK_NEW_ENTRY_SUCCESS: 'ADDRESS_BOOK_NEW_ENTRY_SUCCESS', ADDRESS_BOOK_NEW_ENTRY_SUCCESS: 'ADDRESS_BOOK_NEW_ENTRY_SUCCESS',
ADDRESS_BOOK_EDIT_ENTRY_SUCCESS: 'ADDRESS_BOOK_EDIT_ENTRY_SUCCESS', ADDRESS_BOOK_EDIT_ENTRY_SUCCESS: 'ADDRESS_BOOK_EDIT_ENTRY_SUCCESS',
@ -193,12 +192,12 @@ export const NOTIFICATIONS: Record<NotificationId, Notification> = {
}, },
// Network // Network
RINKEBY_VERSION_MSG: { TESTNET_VERSION_MSG: {
message: "Rinkeby Version: Don't send Mainnet assets to this Safe", message: "Testnet Version: Don't send production assets to this Safe",
options: { variant: WARNING, persist: true, preventDuplicate: true }, options: { variant: WARNING, persist: true, preventDuplicate: true },
}, },
WRONG_NETWORK_MSG: { WRONG_NETWORK_MSG: {
message: `Wrong network: Please use ${capitalize(getNetwork())}`, message: `Wrong network: Please use ${getNetworkName()}`,
options: { variant: WARNING, persist: true, preventDuplicate: true }, options: { variant: WARNING, persist: true, preventDuplicate: true },
}, },

View File

@ -1,6 +1,14 @@
import { useState, useMemo } from 'react' import { useState, useMemo } from 'react'
const INITIAL_STATE = { type SafeActionsState = {
sendFunds: {
isOpen: boolean
selectedToken?: string
}
showReceive: boolean
}
const INITIAL_STATE: SafeActionsState = {
sendFunds: { sendFunds: {
isOpen: false, isOpen: false,
selectedToken: undefined, selectedToken: undefined,
@ -13,7 +21,7 @@ type Response = {
onHide: (action: string) => void onHide: (action: string) => void
showSendFunds: (token: string) => void showSendFunds: (token: string) => void
hideSendFunds: () => void hideSendFunds: () => void
safeActionsState: Record<string, unknown> safeActionsState: SafeActionsState
} }
const useSafeActions = (): Response => { const useSafeActions = (): Response => {

View File

@ -572,18 +572,6 @@ describe('calculateTransactionStatus', () => {
// then // then
expect(result).toBe(TransactionStatus.PENDING) expect(result).toBe(TransactionStatus.PENDING)
}) })
it('It should return PENDING if the tx has no confirmations', () => {
// given
const transaction = makeTransaction({ confirmations: List(), isPending: false })
const safe = makeSafe({ threshold: 3 })
const currentUser = safeAddress
// when
const result = calculateTransactionStatus(transaction, safe, currentUser)
// then
expect(result).toBe(TransactionStatus.PENDING)
})
it('It should return AWAITING_CONFIRMATIONS if the tx has confirmations bellow the threshold, the user is owner and signed', () => { it('It should return AWAITING_CONFIRMATIONS if the tx has confirmations bellow the threshold, the user is owner and signed', () => {
// given // given
const userAddress = 'address1' const userAddress = 'address1'
@ -762,7 +750,36 @@ describe('calculateTransactionType', () => {
describe('buildTx', () => { describe('buildTx', () => {
it('Returns a valid transaction', async () => { it('Returns a valid transaction', async () => {
// given // given
const cancelTx1 = makeTransaction() const cancelTx1 = {
baseGas: 0,
blockNumber: 0,
confirmations: [],
confirmationsRequired: 2,
data: null,
dataDecoded: undefined,
ethGasPrice: '0',
executionDate: null,
executor: '',
fee: '',
gasPrice: '',
gasToken: '',
gasUsed: 0,
isExecuted: false,
isSuccessful: true,
modified: '',
nonce: 0,
operation: 0,
origin: null,
refundReceiver: '',
safe: '',
safeTxGas: 0,
safeTxHash: '',
signatures: '',
submissionDate: null,
to: '',
transactionHash: null,
value: '',
}
const transaction = getMockedTxServiceModel({ to: safeAddress2, value: '0' }) const transaction = getMockedTxServiceModel({ to: safeAddress2, value: '0' })
const userAddress = 'address1' const userAddress = 'address1'
const cancellationTxs = List([cancelTx1]) const cancellationTxs = List([cancelTx1])
@ -776,7 +793,7 @@ describe('buildTx', () => {
}) })
const knownTokens = Map<string, Record<TokenProps> & Readonly<TokenProps>>() const knownTokens = Map<string, Record<TokenProps> & Readonly<TokenProps>>()
knownTokens.set('0x00Df91984582e6e96288307E9c2f20b38C8FeCE9', token) knownTokens.set('0x00Df91984582e6e96288307E9c2f20b38C8FeCE9', token)
const outgoingTxs = List([cancelTx1]) const outgoingTxs = [cancelTx1]
const safeInstance = makeSafe({ name: 'LOADED SAFE', address: safeAddress }) const safeInstance = makeSafe({ name: 'LOADED SAFE', address: safeAddress })
const expectedTx = makeTransaction({ const expectedTx = makeTransaction({
baseGas: 0, baseGas: 0,
@ -826,7 +843,7 @@ describe('buildTx', () => {
outgoingTxs, outgoingTxs,
safe: safeInstance, safe: safeInstance,
tx: transaction, tx: transaction,
txCode: null, txCode: undefined,
}) })
// then // then

View File

@ -1,9 +1,18 @@
import { createAction } from 'redux-actions' import { createAction } from 'redux-actions'
import { SafeRecordProps } from '../models/safe' import { SafeOwner, SafeRecordProps } from '../models/safe'
import { List } from 'immutable'
import { makeOwner } from '../models/owner'
export const ADD_OR_UPDATE_SAFE = 'ADD_OR_UPDATE_SAFE' export const ADD_OR_UPDATE_SAFE = 'ADD_OR_UPDATE_SAFE'
export const addOrUpdateSafe = createAction(ADD_OR_UPDATE_SAFE, (safe: SafeRecordProps) => ({ export const buildOwnersFrom = (names: string[], addresses: string[]): List<SafeOwner> => {
const owners = names.map((name, index) => makeOwner({ name, address: addresses[index] }))
return List(owners)
}
export const addOrUpdateSafe = createAction(ADD_OR_UPDATE_SAFE, (safe: SafeRecordProps, loadedFromStorage = false) => ({
safe, safe,
loadedFromStorage,
})) }))

View File

@ -1,37 +0,0 @@
import { List } from 'immutable'
import { createAction } from 'redux-actions'
import setDefaultSafe from 'src/logic/safe/store/actions/setDefaultSafe'
import { makeOwner } from 'src/logic/safe/store/models/owner'
import { safesListSelector } from 'src/logic/safe/store/selectors'
import { Dispatch } from 'redux'
import { AppReduxState } from 'src/store'
import { SafeOwner, SafeRecordProps } from 'src/logic/safe/store/models/safe'
export const ADD_SAFE = 'ADD_SAFE'
export const buildOwnersFrom = (names: string[], addresses: string[]): List<SafeOwner> => {
const owners = names.map((name, index) => makeOwner({ name, address: addresses[index] }))
return List(owners)
}
export const addSafe = createAction(ADD_SAFE, (safe: SafeRecordProps, loadedFromStorage = false) => ({
safe,
loadedFromStorage,
}))
const saveSafe = (safe: SafeRecordProps) => (dispatch: Dispatch, getState: () => AppReduxState): void => {
const state = getState()
const safeList = safesListSelector(state)
dispatch(addSafe(safe, true))
if (safeList.size === 0) {
dispatch(setDefaultSafe(safe.address))
}
}
export default saveSafe

View File

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

View File

@ -1,5 +1,7 @@
import GnosisSafeSol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafe.json' import GnosisSafeSol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafe.json'
import { List, Set, Map } from 'immutable' import { List, Set, Map } from 'immutable'
import { Action, Dispatch } from 'redux'
import { AbiItem } from 'web3-utils'
import generateBatchRequests from 'src/logic/contracts/generateBatchRequests' import generateBatchRequests from 'src/logic/contracts/generateBatchRequests'
import { getLocalSafe, getSafeName } from 'src/logic/safe/utils' import { getLocalSafe, getSafeName } from 'src/logic/safe/utils'
@ -10,10 +12,8 @@ import addSafeOwner from 'src/logic/safe/store/actions/addSafeOwner'
import removeSafeOwner from 'src/logic/safe/store/actions/removeSafeOwner' import removeSafeOwner from 'src/logic/safe/store/actions/removeSafeOwner'
import updateSafe from 'src/logic/safe/store/actions/updateSafe' import updateSafe from 'src/logic/safe/store/actions/updateSafe'
import { makeOwner } from 'src/logic/safe/store/models/owner' import { makeOwner } from 'src/logic/safe/store/models/owner'
import { checksumAddress } from 'src/utils/checksumAddress' import { checksumAddress } from 'src/utils/checksumAddress'
import { ModulePair, SafeOwner, SafeRecordProps } from 'src/logic/safe/store/models/safe' import { ModulePair, SafeOwner, SafeRecordProps } from 'src/logic/safe/store/models/safe'
import { Action, Dispatch } from 'redux'
import { SENTINEL_ADDRESS } from 'src/logic/contracts/safeContracts' import { SENTINEL_ADDRESS } from 'src/logic/contracts/safeContracts'
import { AppReduxState } from 'src/store' import { AppReduxState } from 'src/store'
import { latestMasterContractVersionSelector } from '../selectors' import { latestMasterContractVersionSelector } from '../selectors'
@ -40,8 +40,8 @@ const buildOwnersFrom = (safeOwners: string[], localSafe?: SafeRecordProps): Lis
return List(ownersList) return List(ownersList)
} }
const buildModulesLinkedList = (modules: string[] | undefined, nextModule: string): Array<ModulePair> | null => { const buildModulesLinkedList = (modules?: string[], nextModule?: string): Array<ModulePair> | null => {
if (modules?.length) { if (modules?.length && nextModule) {
return modules.map((moduleAddress, index, modules) => { return modules.map((moduleAddress, index, modules) => {
const prevModule = modules[index + 1] const prevModule = modules[index + 1]
return [moduleAddress, prevModule !== undefined ? prevModule : nextModule] return [moduleAddress, prevModule !== undefined ? prevModule : nextModule]
@ -58,9 +58,9 @@ export const buildSafe = async (
const safeAddress = checksumAddress(safeAdd) const safeAddress = checksumAddress(safeAdd)
const safeParams = ['getThreshold', 'nonce', 'VERSION', 'getOwners'] const safeParams = ['getThreshold', 'nonce', 'VERSION', 'getOwners']
const [[thresholdStr, nonceStr, currentVersion, remoteOwners], localSafe, ethBalance] = await Promise.all([ const [[, thresholdStr, nonceStr, currentVersion, remoteOwners = []], localSafe, ethBalance] = await Promise.all([
generateBatchRequests({ generateBatchRequests<[undefined, string | undefined, string | undefined, string | undefined, string[]]>({
abi: GnosisSafeSol.abi, abi: GnosisSafeSol.abi as AbiItem[],
address: safeAddress, address: safeAddress,
methods: safeParams, methods: safeParams,
}), }),
@ -81,7 +81,7 @@ export const buildSafe = async (
owners, owners,
ethBalance, ethBalance,
nonce, nonce,
currentVersion, currentVersion: currentVersion ?? '',
needsUpdate, needsUpdate,
featuresEnabled, featuresEnabled,
balances: Map(), balances: Map(),
@ -104,9 +104,23 @@ export const checkAndUpdateSafe = (safeAdd: string) => async (dispatch: Dispatch
// TODO: 100 is an arbitrary large number, to avoid the need for pagination. But pagination must be properly handled // TODO: 100 is an arbitrary large number, to avoid the need for pagination. But pagination must be properly handled
{ method: 'getModulesPaginated', args: [SENTINEL_ADDRESS, 100] }, { method: 'getModulesPaginated', args: [SENTINEL_ADDRESS, 100] },
] ]
const [[remoteThreshold, remoteNonce, remoteOwners, modules], localSafe] = await Promise.all([ const [[, remoteThreshold, remoteNonce, remoteOwners, modules], localSafe] = await Promise.all([
generateBatchRequests({ generateBatchRequests<
abi: GnosisSafeSol.abi, [
undefined,
string | undefined,
string | undefined,
string[],
(
| {
array: string[]
next: string
}
| undefined
),
]
>({
abi: GnosisSafeSol.abi as AbiItem[],
address: safeAddress, address: safeAddress,
methods: safeParams, methods: safeParams,
}), }),
@ -123,6 +137,9 @@ export const checkAndUpdateSafe = (safeAdd: string) => async (dispatch: Dispatch
modules: buildModulesLinkedList(modules?.array, modules?.next), modules: buildModulesLinkedList(modules?.array, modules?.next),
nonce: Number(remoteNonce), nonce: Number(remoteNonce),
threshold: Number(remoteThreshold), threshold: Number(remoteThreshold),
featuresEnabled: localSafe?.currentVersion
? enabledFeatures(localSafe?.currentVersion)
: localSafe?.featuresEnabled,
}), }),
) )

View File

@ -1,7 +1,7 @@
import axios from 'axios' import axios from 'axios'
import { List } from 'immutable' import { List } from 'immutable'
import { buildSafeCreationTxUrl } from 'src/config' import { buildSafeCreationTxUrl } from 'src/logic/safe/utils/buildSafeCreationTxUrl'
import { addOrUpdateTransactions } from './transactions/addOrUpdateTransactions' import { addOrUpdateTransactions } from './transactions/addOrUpdateTransactions'
import { makeTransaction } from 'src/logic/safe/store/models/transaction' import { makeTransaction } from 'src/logic/safe/store/models/transaction'
import { TransactionTypes, TransactionStatus } from 'src/logic/safe/store/models/types/transaction' import { TransactionTypes, TransactionStatus } from 'src/logic/safe/store/models/types/transaction'

View File

@ -5,7 +5,7 @@ import { SafeRecordProps } from 'src/logic/safe/store/models/safe'
import { buildSafe } from 'src/logic/safe/store/reducer/safe' import { buildSafe } from 'src/logic/safe/store/reducer/safe'
import { loadFromStorage } from 'src/utils/storage' import { loadFromStorage } from 'src/utils/storage'
import { addSafe } from './addSafe' import { addOrUpdateSafe } from './addOrUpdateSafe'
const loadSafesFromStorage = () => async (dispatch: Dispatch): Promise<void> => { const loadSafesFromStorage = () => async (dispatch: Dispatch): Promise<void> => {
try { try {
@ -13,7 +13,7 @@ const loadSafesFromStorage = () => async (dispatch: Dispatch): Promise<void> =>
if (safes) { if (safes) {
Object.values(safes).forEach((safeProps) => { Object.values(safes).forEach((safeProps) => {
dispatch(addSafe(buildSafe(safeProps), true)) dispatch(addOrUpdateSafe(buildSafe(safeProps), true))
}) })
} }
} catch (err) { } catch (err) {

View File

@ -1,6 +1,9 @@
import bn from 'bignumber.js' import bn from 'bignumber.js'
import { List, Map } from 'immutable' import { List, Map } from 'immutable'
import { Transaction, TransactionReceipt } from 'web3-core'
import { AbiItem } from 'web3-utils'
import { getNetworkInfo } from 'src/config'
import generateBatchRequests from 'src/logic/contracts/generateBatchRequests' import generateBatchRequests from 'src/logic/contracts/generateBatchRequests'
import { ALTERNATIVE_TOKEN_ABI } from 'src/logic/tokens/utils/alternativeAbi' import { ALTERNATIVE_TOKEN_ABI } from 'src/logic/tokens/utils/alternativeAbi'
import { web3ReadOnly } from 'src/logic/wallets/getWeb3' import { web3ReadOnly } from 'src/logic/wallets/getWeb3'
@ -43,6 +46,7 @@ const buildIncomingTransactionFrom = ([tx, symbol, decimals, fee]: [
const batchIncomingTxsTokenDataRequest = (txs: IncomingTxServiceModel[]) => { const batchIncomingTxsTokenDataRequest = (txs: IncomingTxServiceModel[]) => {
const batch = new web3ReadOnly.BatchRequest() const batch = new web3ReadOnly.BatchRequest()
const { nativeCoin } = getNetworkInfo()
const whenTxsValues = txs.map((tx) => { const whenTxsValues = txs.map((tx) => {
const methods = [ const methods = [
@ -52,8 +56,16 @@ const batchIncomingTxsTokenDataRequest = (txs: IncomingTxServiceModel[]) => {
{ method: 'getTransactionReceipt', args: [tx.transactionHash], type: 'eth' }, { method: 'getTransactionReceipt', args: [tx.transactionHash], type: 'eth' },
] ]
return generateBatchRequests({ return generateBatchRequests<
abi: ALTERNATIVE_TOKEN_ABI, [
IncomingTxServiceModel,
string | undefined,
string | undefined,
Transaction | undefined,
TransactionReceipt | undefined,
]
>({
abi: ALTERNATIVE_TOKEN_ABI as AbiItem[],
address: tx.tokenAddress, address: tx.tokenAddress,
batch, batch,
context: tx, context: tx,
@ -64,11 +76,11 @@ const batchIncomingTxsTokenDataRequest = (txs: IncomingTxServiceModel[]) => {
batch.execute() batch.execute()
return Promise.all(whenTxsValues).then((txsValues) => return Promise.all(whenTxsValues).then((txsValues) =>
txsValues.map(([tx, symbol, decimals, { gasPrice }, { gasUsed }]) => [ txsValues.map(([tx, symbol, decimals, ethTx, ethTxReceipt]) => [
tx, tx,
symbol === null ? 'ETH' : symbol, symbol ? symbol : nativeCoin.symbol,
decimals === null ? '18' : decimals, decimals ? decimals : nativeCoin.decimals,
new bn(gasPrice).times(gasUsed), new bn(ethTx?.gasPrice ?? 0).times(ethTxReceipt?.gasUsed ?? 0),
]), ]),
) )
} }

View File

@ -1,7 +1,7 @@
import { fromJS, List, Map } from 'immutable' import { fromJS, List, Map } from 'immutable'
import generateBatchRequests from 'src/logic/contracts/generateBatchRequests' import generateBatchRequests from 'src/logic/contracts/generateBatchRequests'
import { TOKEN_REDUCER_ID } from 'src/logic/tokens/store/reducer/tokens' import { TOKEN_REDUCER_ID, TokenState } from 'src/logic/tokens/store/reducer/tokens'
import { web3ReadOnly } from 'src/logic/wallets/getWeb3' import { web3ReadOnly } from 'src/logic/wallets/getWeb3'
import { PROVIDER_REDUCER_ID } from 'src/logic/wallets/store/reducer/provider' import { PROVIDER_REDUCER_ID } from 'src/logic/wallets/store/reducer/provider'
import { buildTx, isCancelTransaction } from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers' import { buildTx, isCancelTransaction } from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers'
@ -59,8 +59,8 @@ export type SafeTransactionsType = {
} }
export type OutgoingTxs = { export type OutgoingTxs = {
cancellationTxs: any cancellationTxs: Record<number, TxServiceModel>
outgoingTxs: any outgoingTxs: TxServiceModel[]
} }
export type BatchProcessTxsProps = OutgoingTxs & { export type BatchProcessTxsProps = OutgoingTxs & {
@ -97,12 +97,14 @@ const extractCancelAndOutgoingTxs = (safeAddress: string, outgoingTxs: TxService
) )
} }
type BatchRequestReturnValues = [TxServiceModel, string | undefined]
/** /**
* Requests Contract's code for all the Contracts the Safe has interacted with * Requests Contract's code for all the Contracts the Safe has interacted with
* @param transactions * @param transactions
* @returns {Promise<[Promise<*[]>, Promise<*[]>, Promise<*[]>, Promise<*[]>, Promise<*[]>, Promise<*[]>, Promise<*[]>, Promise<*[]>, Promise<*[]>, Promise<*[]>]>} * @returns {Promise<[Promise<*[]>, Promise<*[]>, Promise<*[]>, Promise<*[]>, Promise<*[]>, Promise<*[]>, Promise<*[]>, Promise<*[]>, Promise<*[]>, Promise<*[]>]>}
*/ */
const batchRequestContractCode = (transactions: any[]): Promise<any[]> => { const batchRequestContractCode = (transactions: TxServiceModel[]): Promise<BatchRequestReturnValues[]> => {
if (!transactions || !Array.isArray(transactions)) { if (!transactions || !Array.isArray(transactions)) {
throw new Error('`transactions` must be provided in order to lookup information') throw new Error('`transactions` must be provided in order to lookup information')
} }
@ -110,7 +112,7 @@ const batchRequestContractCode = (transactions: any[]): Promise<any[]> => {
const batch = new web3ReadOnly.BatchRequest() const batch = new web3ReadOnly.BatchRequest()
const whenTxsValues = transactions.map((tx) => { const whenTxsValues = transactions.map((tx) => {
return generateBatchRequests({ return generateBatchRequests<BatchRequestReturnValues>({
abi: [], abi: [],
address: tx.to, address: tx.to,
batch, batch,
@ -141,7 +143,7 @@ const batchProcessOutgoingTransactions = async ({
safe, safe,
}: BatchProcessTxsProps): Promise<{ }: BatchProcessTxsProps): Promise<{
cancel: Record<string, Transaction> cancel: Record<string, Transaction>
outgoing: Array<Transaction> outgoing: Transaction[]
}> => { }> => {
// cancellation transactions // cancellation transactions
const cancelTxsValues = Object.values(cancellationTxs) const cancelTxsValues = Object.values(cancellationTxs)
@ -193,9 +195,9 @@ export const loadOutgoingTransactions = async (safeAddress: string): Promise<Saf
return defaultResponse return defaultResponse
} }
const knownTokens = state[TOKEN_REDUCER_ID] const knownTokens: TokenState = state[TOKEN_REDUCER_ID]
const currentUser = state[PROVIDER_REDUCER_ID].get('account') const currentUser: string = state[PROVIDER_REDUCER_ID].get('account')
const safe = state[SAFE_REDUCER_ID].getIn(['safes', safeAddress]) const safe: SafeRecord = state[SAFE_REDUCER_ID].getIn(['safes', safeAddress])
if (!safe) { if (!safe) {
return defaultResponse return defaultResponse

View File

@ -1,6 +1,6 @@
import { List, Map } from 'immutable' import { List, Map } from 'immutable'
import { getNetworkInfo } from 'src/config'
import { TOKEN_REDUCER_ID } from 'src/logic/tokens/store/reducer/tokens' import { TOKEN_REDUCER_ID, TokenState } from 'src/logic/tokens/store/reducer/tokens'
import { import {
getERC20DecimalsAndSymbol, getERC20DecimalsAndSymbol,
getERC721Symbol, getERC721Symbol,
@ -78,15 +78,15 @@ export const isUpgradeTransaction = (tx: TxServiceModel): boolean => {
) )
} }
export const isOutgoingTransaction = (tx: TxServiceModel, safeAddress: string): boolean => { export const isOutgoingTransaction = (tx: TxServiceModel, safeAddress?: string): boolean => {
return !sameAddress(tx.to, safeAddress) && !isEmptyData(tx.data) return !sameAddress(tx.to, safeAddress) && !isEmptyData(tx.data)
} }
export const isCustomTransaction = async ( export const isCustomTransaction = async (
tx: TxServiceModel, tx: TxServiceModel,
txCode: string | null, txCode?: string,
safeAddress: string, safeAddress?: string,
knownTokens: Map<string, Token>, knownTokens?: TokenState,
): Promise<boolean> => { ): Promise<boolean> => {
const isOutgoing = isOutgoingTransaction(tx, safeAddress) const isOutgoing = isOutgoingTransaction(tx, safeAddress)
const isErc20 = await isSendERC20Transaction(tx, txCode, knownTokens) const isErc20 = await isSendERC20Transaction(tx, txCode, knownTokens)
@ -100,12 +100,13 @@ export const getRefundParams = async (
tx: TxServiceModel, tx: TxServiceModel,
tokenInfo: (string) => Promise<{ decimals: number; symbol: string } | null>, tokenInfo: (string) => Promise<{ decimals: number; symbol: string } | null>,
): Promise<RefundParams | null> => { ): Promise<RefundParams | null> => {
const { nativeCoin } = getNetworkInfo()
const txGasPrice = Number(tx.gasPrice) const txGasPrice = Number(tx.gasPrice)
let refundParams: RefundParams | null = null let refundParams: RefundParams | null = null
if (txGasPrice > 0) { if (txGasPrice > 0) {
let refundSymbol = 'ETH' let refundSymbol = nativeCoin.symbol
let refundDecimals = 18 let refundDecimals = nativeCoin.decimals
if (tx.gasToken !== ZERO_ADDRESS) { if (tx.gasToken !== ZERO_ADDRESS) {
const gasToken = await tokenInfo(tx.gasToken) const gasToken = await tokenInfo(tx.gasToken)
@ -161,7 +162,7 @@ export const getConfirmations = (tx: TxServiceModel): List<Confirmation> => {
export const isTransactionCancelled = ( export const isTransactionCancelled = (
tx: TxServiceModel, tx: TxServiceModel,
outgoingTxs: Array<TxServiceModel>, outgoingTxs: Array<TxServiceModel>,
cancellationTxs: { number: TxServiceModel }, cancellationTxs: Record<string, TxServiceModel>,
): boolean => { ): boolean => {
return ( return (
// not executed // not executed
@ -175,20 +176,20 @@ export const isTransactionCancelled = (
export const calculateTransactionStatus = ( export const calculateTransactionStatus = (
tx: Transaction, tx: Transaction,
{ owners, threshold }: SafeRecord, { owners, threshold, nonce }: SafeRecord,
currentUser?: string | null, currentUser?: string | null,
): TransactionStatusValues => { ): TransactionStatusValues => {
let txStatus let txStatus
if (tx.isExecuted && tx.isSuccessful) { if (tx.isExecuted && tx.isSuccessful) {
txStatus = TransactionStatus.SUCCESS txStatus = TransactionStatus.SUCCESS
} else if (tx.cancelled) { } else if (tx.cancelled || nonce > tx.nonce) {
txStatus = TransactionStatus.CANCELLED txStatus = TransactionStatus.CANCELLED
} else if (tx.confirmations.size === threshold) { } else if (tx.confirmations.size === threshold) {
txStatus = TransactionStatus.AWAITING_EXECUTION txStatus = TransactionStatus.AWAITING_EXECUTION
} else if (tx.creationTx) { } else if (tx.creationTx) {
txStatus = TransactionStatus.SUCCESS txStatus = TransactionStatus.SUCCESS
} else if (!tx.confirmations.size || !!tx.isPending) { } else if (!!tx.isPending) {
txStatus = TransactionStatus.PENDING txStatus = TransactionStatus.PENDING
} else { } else {
const userConfirmed = tx.confirmations.filter((conf) => conf.owner === currentUser).size === 1 const userConfirmed = tx.confirmations.filter((conf) => conf.owner === currentUser).size === 1
@ -230,7 +231,7 @@ export const calculateTransactionType = (tx: Transaction): TransactionTypeValues
export type BuildTx = BatchProcessTxsProps & { export type BuildTx = BatchProcessTxsProps & {
tx: TxServiceModel tx: TxServiceModel
txCode: string | null txCode?: string
} }
export const buildTx = async ({ export const buildTx = async ({
@ -243,6 +244,7 @@ export const buildTx = async ({
txCode, txCode,
}: BuildTx): Promise<Transaction> => { }: BuildTx): Promise<Transaction> => {
const safeAddress = safe.address const safeAddress = safe.address
const { nativeCoin } = getNetworkInfo()
const isModifySettingsTx = isModifySettingsTransaction(tx, safeAddress) const isModifySettingsTx = isModifySettingsTransaction(tx, safeAddress)
const isTxCancelled = isTransactionCancelled(tx, outgoingTxs, cancellationTxs) const isTxCancelled = isTransactionCancelled(tx, outgoingTxs, cancellationTxs)
const isSendERC721Tx = isSendERC721Transaction(tx, txCode, knownTokens) const isSendERC721Tx = isSendERC721Transaction(tx, txCode, knownTokens)
@ -255,8 +257,8 @@ export const buildTx = async ({
const decodedParams = getDecodedParams(tx) const decodedParams = getDecodedParams(tx)
const confirmations = getConfirmations(tx) const confirmations = getConfirmations(tx)
let tokenDecimals = 18 let tokenDecimals = nativeCoin.decimals
let tokenSymbol = 'ETH' let tokenSymbol = nativeCoin.symbol
try { try {
if (isSendERC20Tx) { if (isSendERC20Tx) {
const { decimals, symbol } = await getERC20DecimalsAndSymbol(tx.to) const { decimals, symbol } = await getERC20DecimalsAndSymbol(tx.to)

View File

@ -10,7 +10,6 @@ import { userAccountSelector } from 'src/logic/wallets/store/selectors'
import { getIncomingTxAmount } from 'src/routes/safe/components/Transactions/TxsTable/columns' import { getIncomingTxAmount } from 'src/routes/safe/components/Transactions/TxsTable/columns'
import { grantedSelector } from 'src/routes/safe/container/selector' import { grantedSelector } from 'src/routes/safe/container/selector'
import { ADD_INCOMING_TRANSACTIONS } from 'src/logic/safe/store/actions/addIncomingTransactions' import { ADD_INCOMING_TRANSACTIONS } from 'src/logic/safe/store/actions/addIncomingTransactions'
import { ADD_SAFE } from 'src/logic/safe/store/actions/addSafe'
import { ADD_OR_UPDATE_TRANSACTIONS } from 'src/logic/safe/store/actions/transactions/addOrUpdateTransactions' import { ADD_OR_UPDATE_TRANSACTIONS } from 'src/logic/safe/store/actions/transactions/addOrUpdateTransactions'
import updateSafe from 'src/logic/safe/store/actions/updateSafe' import updateSafe from 'src/logic/safe/store/actions/updateSafe'
import { import {
@ -20,8 +19,9 @@ import {
} from 'src/logic/safe/store/selectors' } from 'src/logic/safe/store/selectors'
import { loadFromStorage, saveToStorage } from 'src/utils/storage' import { loadFromStorage, saveToStorage } from 'src/utils/storage'
import { ADD_OR_UPDATE_SAFE } from '../actions/addOrUpdateSafe'
const watchedActions = [ADD_OR_UPDATE_TRANSACTIONS, ADD_INCOMING_TRANSACTIONS, ADD_SAFE] const watchedActions = [ADD_OR_UPDATE_TRANSACTIONS, ADD_INCOMING_TRANSACTIONS, ADD_OR_UPDATE_SAFE]
const sendAwaitingTransactionNotification = async ( const sendAwaitingTransactionNotification = async (
dispatch, dispatch,
@ -146,7 +146,7 @@ const notificationsMiddleware = (store) => (next) => async (action) => {
}) })
break break
} }
case ADD_SAFE: { case ADD_OR_UPDATE_SAFE: {
const state = store.getState() const state = store.getState()
const { safe } = action.payload const { safe } = action.payload
const currentSafeAddress = safeParamAddressFromStateSelector(state) || safe.address const currentSafeAddress = safeParamAddressFromStateSelector(state) || safe.address

View File

@ -1,9 +1,7 @@
import { addAddressBookEntry } from 'src/logic/addressBook/store/actions/addAddressBookEntry'
import { saveDefaultSafe, saveSafes } from 'src/logic/safe/utils' import { saveDefaultSafe, saveSafes } from 'src/logic/safe/utils'
import { tokensSelector } from 'src/logic/tokens/store/selectors' import { tokensSelector } from 'src/logic/tokens/store/selectors'
import { saveActiveTokens } from 'src/logic/tokens/utils/tokensStorage' import { saveActiveTokens } from 'src/logic/tokens/utils/tokensStorage'
import { ACTIVATE_TOKEN_FOR_ALL_SAFES } from 'src/logic/safe/store/actions/activateTokenForAllSafes' import { ACTIVATE_TOKEN_FOR_ALL_SAFES } from 'src/logic/safe/store/actions/activateTokenForAllSafes'
import { ADD_SAFE } from 'src/logic/safe/store/actions/addSafe'
import { ADD_SAFE_OWNER } from 'src/logic/safe/store/actions/addSafeOwner' import { ADD_SAFE_OWNER } from 'src/logic/safe/store/actions/addSafeOwner'
import { EDIT_SAFE_OWNER } from 'src/logic/safe/store/actions/editSafeOwner' import { EDIT_SAFE_OWNER } from 'src/logic/safe/store/actions/editSafeOwner'
import { REMOVE_SAFE } from 'src/logic/safe/store/actions/removeSafe' import { REMOVE_SAFE } from 'src/logic/safe/store/actions/removeSafe'
@ -14,17 +12,13 @@ import { UPDATE_SAFE } from 'src/logic/safe/store/actions/updateSafe'
import { UPDATE_TOKENS_LIST } from 'src/logic/safe/store/actions/updateTokensList' import { UPDATE_TOKENS_LIST } from 'src/logic/safe/store/actions/updateTokensList'
import { UPDATE_ASSETS_LIST } from 'src/logic/safe/store/actions/updateAssetsList' import { UPDATE_ASSETS_LIST } from 'src/logic/safe/store/actions/updateAssetsList'
import { getActiveTokensAddressesForAllSafes, safesMapSelector } from 'src/logic/safe/store/selectors' import { getActiveTokensAddressesForAllSafes, safesMapSelector } from 'src/logic/safe/store/selectors'
import { checksumAddress } from 'src/utils/checksumAddress'
import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
import { addOrUpdateAddressBookEntry } from 'src/logic/addressBook/store/actions/addOrUpdateAddressBookEntry'
import { checkIfEntryWasDeletedFromAddressBook, isValidAddressBookName } from 'src/logic/addressBook/utils'
import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
import { sameAddress } from 'src/logic/wallets/ethAddresses'
import { updateAddressBookEntry } from 'src/logic/addressBook/store/actions/updateAddressBookEntry'
import { ADD_OR_UPDATE_SAFE } from 'src/logic/safe/store/actions/addOrUpdateSafe' import { ADD_OR_UPDATE_SAFE } from 'src/logic/safe/store/actions/addOrUpdateSafe'
import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
import { checksumAddress } from 'src/utils/checksumAddress'
import { isValidAddressBookName } from 'src/logic/addressBook/utils'
import { addOrUpdateAddressBookEntry } from 'src/logic/addressBook/store/actions/addOrUpdateAddressBookEntry'
const watchedActions = [ const watchedActions = [
ADD_SAFE,
UPDATE_SAFE, UPDATE_SAFE,
REMOVE_SAFE, REMOVE_SAFE,
ADD_OR_UPDATE_SAFE, ADD_OR_UPDATE_SAFE,
@ -60,7 +54,6 @@ const safeStorageMware = (store) => (next) => async (action) => {
const state = store.getState() const state = store.getState()
const { dispatch } = store const { dispatch } = store
const safes = safesMapSelector(state) const safes = safesMapSelector(state)
const addressBook = addressBookSelector(state)
await saveSafes(safes.toJSON()) await saveSafes(safes.toJSON())
switch (action.type) { switch (action.type) {
@ -68,43 +61,6 @@ const safeStorageMware = (store) => (next) => async (action) => {
recalculateActiveTokens(state) recalculateActiveTokens(state)
break break
} }
case ADD_SAFE: {
const { safe, loadedFromStorage } = action.payload
const safeAlreadyLoaded =
loadedFromStorage || safes.find((safeIterator) => sameAddress(safeIterator.address, safe.address))
safe.owners.forEach((owner) => {
const checksumEntry = makeAddressBookEntry({ address: checksumAddress(owner.address), name: owner.name })
const ownerWasAlreadyInAddressBook = checkIfEntryWasDeletedFromAddressBook(
checksumEntry,
addressBook,
safeAlreadyLoaded,
)
if (!ownerWasAlreadyInAddressBook) {
dispatch(addAddressBookEntry(checksumEntry, { notifyEntryUpdate: false }))
}
const addressAlreadyExists = addressBook.find((entry) => sameAddress(entry.address, checksumEntry.address))
if (isValidAddressBookName(checksumEntry.name) && addressAlreadyExists) {
dispatch(updateAddressBookEntry(checksumEntry))
}
})
const safeWasAlreadyInAddressBook = checkIfEntryWasDeletedFromAddressBook(
{ address: safe.address, name: safe.name },
addressBook,
safeAlreadyLoaded,
)
if (!safeWasAlreadyInAddressBook) {
dispatch(
addAddressBookEntry(makeAddressBookEntry({ address: safe.address, name: safe.name }), {
notifyEntryUpdate: true,
}),
)
}
break
}
case ADD_OR_UPDATE_SAFE: { case ADD_OR_UPDATE_SAFE: {
const { safe } = action.payload const { safe } = action.payload
safe.owners.forEach((owner) => { safe.owners.forEach((owner) => {

View File

@ -1,4 +1,5 @@
import { List, Map, Record, RecordOf, Set } from 'immutable' import { List, Map, Record, RecordOf, Set } from 'immutable'
import { FEATURES } from 'src/config/networks/network.d'
export type SafeOwner = { export type SafeOwner = {
name: string name: string
@ -24,7 +25,7 @@ export type SafeRecordProps = {
recurringUser?: boolean recurringUser?: boolean
currentVersion: string currentVersion: string
needsUpdate: boolean needsUpdate: boolean
featuresEnabled: Array<string> featuresEnabled: Array<FEATURES>
} }
const makeSafe = Record<SafeRecordProps>({ const makeSafe = Record<SafeRecordProps>({

View File

@ -31,6 +31,7 @@ export const makeTransaction = Record<TransactionProps>({
isCancellationTx: false, isCancellationTx: false,
isCollectibleTransfer: false, isCollectibleTransfer: false,
isExecuted: false, isExecuted: false,
isPending: false,
isSuccessful: true, isSuccessful: true,
isTokenTransfer: false, isTokenTransfer: false,
masterCopy: '', masterCopy: '',

View File

@ -2,7 +2,6 @@ import { Map, Set, List } from 'immutable'
import { handleActions } from 'redux-actions' import { handleActions } from 'redux-actions'
import { ACTIVATE_TOKEN_FOR_ALL_SAFES } from 'src/logic/safe/store/actions/activateTokenForAllSafes' import { ACTIVATE_TOKEN_FOR_ALL_SAFES } from 'src/logic/safe/store/actions/activateTokenForAllSafes'
import { ADD_SAFE, buildOwnersFrom } from 'src/logic/safe/store/actions/addSafe'
import { ADD_SAFE_OWNER } from 'src/logic/safe/store/actions/addSafeOwner' import { ADD_SAFE_OWNER } from 'src/logic/safe/store/actions/addSafeOwner'
import { EDIT_SAFE_OWNER } from 'src/logic/safe/store/actions/editSafeOwner' import { EDIT_SAFE_OWNER } from 'src/logic/safe/store/actions/editSafeOwner'
import { REMOVE_SAFE } from 'src/logic/safe/store/actions/removeSafe' import { REMOVE_SAFE } from 'src/logic/safe/store/actions/removeSafe'
@ -17,7 +16,7 @@ import { makeOwner } from 'src/logic/safe/store/models/owner'
import makeSafe, { SafeRecordProps } from 'src/logic/safe/store/models/safe' import makeSafe, { SafeRecordProps } from 'src/logic/safe/store/models/safe'
import { checksumAddress } from 'src/utils/checksumAddress' import { checksumAddress } from 'src/utils/checksumAddress'
import { SafeReducerMap } from 'src/routes/safe/store/reducer/types/safe' import { SafeReducerMap } from 'src/routes/safe/store/reducer/types/safe'
import { ADD_OR_UPDATE_SAFE } from 'src/logic/safe/store/actions/addOrUpdateSafe' import { ADD_OR_UPDATE_SAFE, buildOwnersFrom } from 'src/logic/safe/store/actions/addOrUpdateSafe'
import { sameAddress } from 'src/logic/wallets/ethAddresses' import { sameAddress } from 'src/logic/wallets/ethAddresses'
export const SAFE_REDUCER_ID = 'safes' export const SAFE_REDUCER_ID = 'safes'
@ -99,19 +98,7 @@ export default handleActions(
}) })
}) })
}, },
[ADD_SAFE]: (state: SafeReducerMap, action) => {
const { safe } = action.payload
// if you add a new Safe it needs to be set as a record
// in case of update it shouldn't, because a record would be initialized
// with initial props and it would overwrite existing ones
if (state.hasIn(['safes', safe.address])) {
return state
}
return state.setIn(['safes', safe.address], makeSafe(safe))
},
[ADD_OR_UPDATE_SAFE]: (state: SafeReducerMap, action) => { [ADD_OR_UPDATE_SAFE]: (state: SafeReducerMap, action) => {
const { safe } = action.payload const { safe } = action.payload

View File

@ -21,7 +21,7 @@ export default handleActions(
if (stateTransactionsList) { if (stateTransactionsList) {
const txsToStore = stateTransactionsList.withMutations((txsList) => { const txsToStore = stateTransactionsList.withMutations((txsList) => {
transactions.forEach((updateTx) => { transactions.forEach((updateTx) => {
const storedTxIndex = txsList.findIndex((txIterator) => txIterator.nonce === updateTx.nonce) const storedTxIndex = txsList.findIndex((txIterator) => txIterator.safeTxHash === updateTx.safeTxHash)
if (storedTxIndex !== -1) { if (storedTxIndex !== -1) {
// Update // Update

View File

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

View File

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

View File

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

View File

@ -3,15 +3,24 @@ import semverSatisfies from 'semver/functions/satisfies'
import semverValid from 'semver/functions/valid' import semverValid from 'semver/functions/valid'
import { GnosisSafe } from 'src/types/contracts/GnosisSafe.d' import { GnosisSafe } from 'src/types/contracts/GnosisSafe.d'
import { getSafeLastVersion } from 'src/config'
import { getGnosisSafeInstanceAt, getSafeMasterContract } from 'src/logic/contracts/safeContracts' import { getGnosisSafeInstanceAt, getSafeMasterContract } from 'src/logic/contracts/safeContracts'
import { LATEST_SAFE_VERSION } from 'src/utils/constants'
import { getNetworkConfigDisabledFeatures } from 'src/config'
import { FEATURES } from 'src/config/networks/network.d'
export const FEATURES = [ type FeatureConfigByVersion = {
{ name: 'ERC721', validVersion: '>=1.1.1' }, name: FEATURES
{ name: 'ERC1155', validVersion: '>=1.1.1' }, validVersion?: string
}
const FEATURES_BY_VERSION: FeatureConfigByVersion[] = [
{ name: FEATURES.ERC721, validVersion: '>=1.1.1' },
{ name: FEATURES.ERC1155, validVersion: '>=1.1.1' },
{ name: FEATURES.SAFE_APPS },
{ name: FEATURES.CONTRACT_INTERACTION },
] ]
type Feature = typeof FEATURES[number] type Feature = typeof FEATURES_BY_VERSION[number]
export const safeNeedsUpdate = (currentVersion?: string, latestVersion?: string): boolean => { export const safeNeedsUpdate = (currentVersion?: string, latestVersion?: string): boolean => {
if (!currentVersion || !latestVersion) { if (!currentVersion || !latestVersion) {
@ -27,13 +36,19 @@ export const safeNeedsUpdate = (currentVersion?: string, latestVersion?: string)
export const getCurrentSafeVersion = (gnosisSafeInstance: GnosisSafe): Promise<string> => export const getCurrentSafeVersion = (gnosisSafeInstance: GnosisSafe): Promise<string> =>
gnosisSafeInstance.methods.VERSION().call() gnosisSafeInstance.methods.VERSION().call()
export const enabledFeatures = (version: string): string[] => const checkFeatureEnabledByVersion = (featureConfig: FeatureConfigByVersion, version: string) => {
FEATURES.reduce((acc: string[], feature: Feature) => { return featureConfig.validVersion ? semverSatisfies(version, featureConfig.validVersion) : true
if (semverSatisfies(version, feature.validVersion)) { }
export const enabledFeatures = (version?: string): FEATURES[] => {
const disabledFeatures = getNetworkConfigDisabledFeatures()
return FEATURES_BY_VERSION.reduce((acc: FEATURES[], feature: Feature) => {
if (!disabledFeatures.includes(feature.name) && version && checkFeatureEnabledByVersion(feature, version)) {
acc.push(feature.name) acc.push(feature.name)
} }
return acc return acc
}, []) }, [])
}
interface SafeVersionInfo { interface SafeVersionInfo {
current: string current: string
@ -60,11 +75,11 @@ export const getCurrentMasterContractLastVersion = async (): Promise<string> =>
const safeMaster = await getSafeMasterContract() const safeMaster = await getSafeMasterContract()
let safeMasterVersion let safeMasterVersion
try { try {
safeMasterVersion = await safeMaster.VERSION() safeMasterVersion = await safeMaster.methods.VERSION().call()
} catch (err) { } catch (err) {
// Default in case that it's not possible to obtain the version from the contract, returns a hardcoded value or an // Default in case that it's not possible to obtain the version from the contract, returns a hardcoded value or an
// env variable // env variable
safeMasterVersion = getSafeLastVersion() safeMasterVersion = LATEST_SAFE_VERSION
} }
return safeMasterVersion return safeMasterVersion
} }

View File

@ -0,0 +1,24 @@
import axios, { AxiosResponse } from 'axios'
import { getTxServiceUrl } from 'src/config'
export type TokenResult = {
address: string
decimals?: number
logoUri: string
name: string
symbol: string
type: string
}
export const fetchErc20AndErc721AssetsList = async (): Promise<AxiosResponse<{ results: TokenResult[] }>> => {
const apiUrl = getTxServiceUrl()
const url = `${apiUrl}/tokens/`
return axios.get<{ results: TokenResult[] }>(url, {
params: {
limit: 3000,
},
})
}

View File

@ -0,0 +1,24 @@
import axios, { AxiosResponse } from 'axios'
import { getTxServiceUrl } from 'src/config'
export type CollectibleResult = {
address: string
description: string | null
id: string
imageUri: string | null
logoUri: string
metadata: Record<string, unknown>
name: string | null
tokenName: string
tokenSymbol: string
uri: string | null
}
export const fetchSafeCollectibles = async (safeAddress: string): Promise<AxiosResponse<CollectibleResult[]>> => {
const apiUrl = getTxServiceUrl()
const url = `${apiUrl}/safes/${safeAddress}/collectibles/`
return axios.get(url)
}

View File

@ -1,16 +0,0 @@
import axios from 'axios'
import { getRelayUrl } from 'src/config/index'
const fetchToken = (tokenAddress) => {
const apiUrl = getRelayUrl()
const url = `${apiUrl}/tokens/`
return axios.get(url, {
params: {
address: tokenAddress,
},
})
}
export default fetchToken

View File

@ -1,16 +1,17 @@
import axios from 'axios' import axios, { AxiosResponse } from 'axios'
import { getTxServiceHost } from 'src/config/index' import { getTxServiceUrl } from 'src/config'
import { TokenProps } from 'src/logic/tokens/store/model/token'
const fetchTokenBalanceList = (safeAddress) => { type BalanceResult = {
const apiUrl = getTxServiceHost() tokenAddress: string
const url = `${apiUrl}safes/${safeAddress}/balances/` token: TokenProps
balance: string
return axios.get(url, {
params: {
limit: 3000,
},
})
} }
export default fetchTokenBalanceList export const fetchTokenBalanceList = (safeAddress: string): Promise<AxiosResponse<{ results: BalanceResult[] }>> => {
const apiUrl = getTxServiceUrl()
const url = `${apiUrl}/safes/${safeAddress}/balances/`
return axios.get(url)
}

View File

@ -1,16 +0,0 @@
import axios from 'axios'
import { getRelayUrl } from 'src/config/index'
const fetchTokenList = () => {
const apiUrl = getRelayUrl()
const url = `${apiUrl}tokens/`
return axios.get(url, {
params: {
limit: 3000,
},
})
}
export default fetchTokenList

View File

@ -1,2 +1,3 @@
export { default as fetchTokenList } from './fetchTokenList' export { fetchErc20AndErc721AssetsList } from './fetchErc20AndErc721AssetsList'
export { default as fetchToken } from './fetchToken' export { fetchSafeCollectibles } from './fetchSafeCollectibles'
export { fetchTokenBalanceList } from './fetchTokenBalanceList'

View File

@ -3,15 +3,12 @@ import { List, Map } from 'immutable'
import { batch } from 'react-redux' import { batch } from 'react-redux'
import { Dispatch } from 'redux' import { Dispatch } from 'redux'
import fetchTokenCurrenciesBalances, { import {
fetchTokenCurrenciesBalances,
BalanceEndpoint, BalanceEndpoint,
} from 'src/logic/currencyValues/api/fetchTokenCurrenciesBalances' } from 'src/logic/currencyValues/api/fetchTokenCurrenciesBalances'
import { setCurrencyBalances } from 'src/logic/currencyValues/store/actions/setCurrencyBalances' import { setCurrencyBalances } from 'src/logic/currencyValues/store/actions/setCurrencyBalances'
import { import { CurrencyRateValueRecord, makeBalanceCurrency } from 'src/logic/currencyValues/store/model/currencyValues'
AVAILABLE_CURRENCIES,
CurrencyRateValueRecord,
makeBalanceCurrency,
} from 'src/logic/currencyValues/store/model/currencyValues'
import addTokens from 'src/logic/tokens/store/actions/saveTokens' import addTokens from 'src/logic/tokens/store/actions/saveTokens'
import { makeToken, Token } from 'src/logic/tokens/store/model/token' import { makeToken, Token } from 'src/logic/tokens/store/model/token'
import { TokenState } from 'src/logic/tokens/store/reducer/tokens' import { TokenState } from 'src/logic/tokens/store/reducer/tokens'
@ -43,7 +40,7 @@ interface ExtractedData {
const extractDataFromResult = (currentTokens: TokenState) => ( const extractDataFromResult = (currentTokens: TokenState) => (
acc: ExtractedData, acc: ExtractedData,
{ balance, balanceUsd, token, tokenAddress }: BalanceEndpoint, { balance, fiatBalance, fiatCode, token, tokenAddress }: BalanceEndpoint,
): ExtractedData => { ): ExtractedData => {
if (tokenAddress === null) { if (tokenAddress === null) {
acc.ethBalance = humanReadableValue(balance, 18) acc.ethBalance = humanReadableValue(balance, 18)
@ -57,10 +54,10 @@ const extractDataFromResult = (currentTokens: TokenState) => (
acc.currencyList = acc.currencyList.push( acc.currencyList = acc.currencyList.push(
makeBalanceCurrency({ makeBalanceCurrency({
currencyName: balanceUsd ? AVAILABLE_CURRENCIES.USD : undefined, currencyName: fiatCode,
tokenAddress, tokenAddress,
balanceInBaseCurrency: balanceUsd, balanceInBaseCurrency: fiatBalance,
balanceInSelectedCurrency: balanceUsd, balanceInSelectedCurrency: fiatBalance,
}), }),
) )

View File

@ -3,12 +3,13 @@ import HumanFriendlyToken from '@gnosis.pm/util-contracts/build/contracts/HumanF
import ERC20Detailed from '@openzeppelin/contracts/build/contracts/ERC20Detailed.json' import ERC20Detailed from '@openzeppelin/contracts/build/contracts/ERC20Detailed.json'
import ERC721 from '@openzeppelin/contracts/build/contracts/ERC721.json' import ERC721 from '@openzeppelin/contracts/build/contracts/ERC721.json'
import { List } from 'immutable' import { List } from 'immutable'
import contract from 'truffle-contract' import contract from '@truffle/contract/index.js'
import { AbiItem } from 'web3-utils'
import saveTokens from './saveTokens' import saveTokens from './saveTokens'
import generateBatchRequests from 'src/logic/contracts/generateBatchRequests' import generateBatchRequests from 'src/logic/contracts/generateBatchRequests'
import { fetchTokenList } from 'src/logic/tokens/api' import { fetchErc20AndErc721AssetsList } from 'src/logic/tokens/api'
import { makeToken, Token } from 'src/logic/tokens/store/model/token' import { makeToken, Token } from 'src/logic/tokens/store/model/token'
import { tokensSelector } from 'src/logic/tokens/store/selectors' import { tokensSelector } from 'src/logic/tokens/store/selectors'
import { getWeb3 } from 'src/logic/wallets/getWeb3' import { getWeb3 } from 'src/logic/wallets/getWeb3'
@ -53,8 +54,8 @@ export const containsMethodByHash = async (contractAddress: string, methodHash:
} }
const getTokenValues = (tokenAddress) => const getTokenValues = (tokenAddress) =>
generateBatchRequests({ generateBatchRequests<[undefined, string | undefined, string | undefined, string | undefined]>({
abi: ERC20Detailed.abi, abi: ERC20Detailed.abi as AbiItem[],
address: tokenAddress, address: tokenAddress,
methods: ['decimals', 'name', 'symbol'], methods: ['decimals', 'name', 'symbol'],
}) })
@ -69,7 +70,7 @@ export const getTokenInfos = async (tokenAddress: string): Promise<Token | undef
} }
// Otherwise we fetch it, save it to the store and return it // Otherwise we fetch it, save it to the store and return it
const [tokenDecimals, tokenName, tokenSymbol] = await getTokenValues(tokenAddress) const [, tokenDecimals, tokenName, tokenSymbol] = await getTokenValues(tokenAddress)
if (tokenDecimals === null) { if (tokenDecimals === null) {
return undefined return undefined
@ -98,13 +99,15 @@ export const fetchTokens = () => async (
const { const {
data: { results: tokenList }, data: { results: tokenList },
} = await fetchTokenList() } = await fetchErc20AndErc721AssetsList()
if (currentSavedTokens && currentSavedTokens.size === tokenList.length) { const erc20Tokens = tokenList.filter((token) => token.type.toLowerCase() === 'erc20')
if (currentSavedTokens?.size === erc20Tokens.length) {
return return
} }
const tokens = List(tokenList.map((token) => makeToken(token))) const tokens = List(erc20Tokens.map((token) => makeToken(token)))
dispatch(saveTokens(tokens)) dispatch(saveTokens(tokens))
} catch (err) { } catch (err) {

View File

@ -1,6 +1,7 @@
import { makeToken } from 'src/logic/tokens/store/model/token' import { makeToken } from 'src/logic/tokens/store/model/token'
import { getERC20DecimalsAndSymbol, isERC721Contract, isTokenTransfer } from 'src/logic/tokens/utils/tokenHelpers' import { getERC20DecimalsAndSymbol, isERC721Contract, isTokenTransfer } from 'src/logic/tokens/utils/tokenHelpers'
import { getMockedTxServiceModel } from 'src/test/utils/safeHelper' import { getMockedTxServiceModel } from 'src/test/utils/safeHelper'
import { fromTokenUnit, toTokenUnit } from 'src/logic/tokens/utils/humanReadableValue'
describe('isTokenTransfer', () => { describe('isTokenTransfer', () => {
const safeAddress = '0xdfA693da0D16F5E7E78FdCBeDe8FC6eBEa44f1Cf' const safeAddress = '0xdfA693da0D16F5E7E78FdCBeDe8FC6eBEa44f1Cf'
@ -113,7 +114,11 @@ describe('getERC20DecimalsAndSymbol', () => {
const generateBatchRequests = require('src/logic/contracts/generateBatchRequests') const generateBatchRequests = require('src/logic/contracts/generateBatchRequests')
const spyTokenInfos = fetchTokens.getTokenInfos.mockImplementationOnce(() => null) const spyTokenInfos = fetchTokens.getTokenInfos.mockImplementationOnce(() => null)
const spyGenerateBatchRequest = generateBatchRequests.default.mockImplementationOnce(() => [decimals, symbol]) const spyGenerateBatchRequest = generateBatchRequests.default.mockImplementationOnce(() => [
undefined,
decimals,
symbol,
])
// when // when
const result = await getERC20DecimalsAndSymbol(tokenAddress) const result = await getERC20DecimalsAndSymbol(tokenAddress)
@ -171,4 +176,31 @@ describe('isERC721Contract', () => {
expect(result).toEqual(expectedResult) expect(result).toEqual(expectedResult)
expect(standardContractSpy).toHaveBeenCalled() expect(standardContractSpy).toHaveBeenCalled()
}) })
it('It should return the right conversion from unit to token', () => {
// given
const decimals = Number(18)
const expectedResult = '0.000000003'
const ESTIMATED_GAS_COST = 3e9 // 3 Gwei
// when
const gasCosts = fromTokenUnit(ESTIMATED_GAS_COST, decimals)
// then
expect(gasCosts).toEqual(expectedResult)
})
it('It should return the right conversion from token to unit', () => {
// given
const decimals = Number(18)
const expectedResult = '300000000000000000'
const VALUE = 0.3
// when
const txValue = toTokenUnit(VALUE, decimals)
// then
expect(txValue).toEqual(expectedResult)
})
}) })

View File

@ -3,3 +3,17 @@ import { BigNumber } from 'bignumber.js'
export const humanReadableValue = (value: number | string, decimals = 18): string => { export const humanReadableValue = (value: number | string, decimals = 18): string => {
return new BigNumber(value).times(`1e-${decimals}`).toFixed() return new BigNumber(value).times(`1e-${decimals}`).toFixed()
} }
export const fromTokenUnit = (amount: number | string, decimals: string | number): string =>
new BigNumber(amount).times(`1e-${decimals}`).toFixed()
export const toTokenUnit = (amount: number | string, decimals: string | number): string => {
const amountBN = new BigNumber(amount).times(`1e${decimals}`)
const [, amountDecimalPlaces] = amount.toString().split('.')
if (amountDecimalPlaces?.length >= +decimals) {
return amountBN.toFixed(+decimals, BigNumber.ROUND_DOWN)
}
return amountBN.toFixed()
}

View File

@ -1,4 +1,6 @@
import logo from 'src/assets/icons/icon_etherTokens.svg' import { AbiItem } from 'web3-utils'
import { getNetworkInfo } from 'src/config'
import generateBatchRequests from 'src/logic/contracts/generateBatchRequests' import generateBatchRequests from 'src/logic/contracts/generateBatchRequests'
import { import {
getStandardTokenContract, getStandardTokenContract,
@ -6,22 +8,18 @@ import {
getERC721TokenContract, getERC721TokenContract,
} from 'src/logic/tokens/store/actions/fetchTokens' } from 'src/logic/tokens/store/actions/fetchTokens'
import { makeToken, Token } from 'src/logic/tokens/store/model/token' import { makeToken, Token } from 'src/logic/tokens/store/model/token'
import { TokenState } from 'src/logic/tokens/store/reducer/tokens'
import { ALTERNATIVE_TOKEN_ABI } from 'src/logic/tokens/utils/alternativeAbi' import { ALTERNATIVE_TOKEN_ABI } from 'src/logic/tokens/utils/alternativeAbi'
import { web3ReadOnly as web3 } from 'src/logic/wallets/getWeb3' import { web3ReadOnly as web3 } from 'src/logic/wallets/getWeb3'
import { isEmptyData } from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers' import { isEmptyData } from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers'
import { TxServiceModel } from 'src/logic/safe/store/actions/transactions/fetchTransactions/loadOutgoingTransactions' import { TxServiceModel } from 'src/logic/safe/store/actions/transactions/fetchTransactions/loadOutgoingTransactions'
import { Map } from 'immutable'
export const ETH_ADDRESS = '0x000'
export const SAFE_TRANSFER_FROM_WITHOUT_DATA_HASH = '42842e0e' export const SAFE_TRANSFER_FROM_WITHOUT_DATA_HASH = '42842e0e'
export const getEthAsToken = (balance: string | number): Token => { export const getEthAsToken = (balance: string | number): Token => {
const { nativeCoin } = getNetworkInfo()
return makeToken({ return makeToken({
address: ETH_ADDRESS, ...nativeCoin,
name: 'Ether',
symbol: 'ETH',
decimals: 18,
logoUri: logo,
balance, balance,
}) })
} }
@ -44,18 +42,13 @@ export const isTokenTransfer = (tx: TxServiceModel): boolean => {
return !isEmptyData(tx.data) && tx.data?.substring(0, 10) === '0xa9059cbb' && Number(tx.value) === 0 return !isEmptyData(tx.data) && tx.data?.substring(0, 10) === '0xa9059cbb' && Number(tx.value) === 0
} }
export const isSendERC721Transaction = ( export const isSendERC721Transaction = (tx: TxServiceModel, txCode?: string, knownTokens?: TokenState): boolean => {
tx: TxServiceModel,
txCode: string | null,
knownTokens: Map<string, Token>,
): boolean => {
// "0x57f1887a8BF19b14fC0dF6Fd9B2acc9Af147eA85" - ens token contract, includes safeTransferFrom // "0x57f1887a8BF19b14fC0dF6Fd9B2acc9Af147eA85" - ens token contract, includes safeTransferFrom
// but no proper ERC721 standard implemented // but no proper ERC721 standard implemented
return ( return (
(txCode && (txCode?.includes(SAFE_TRANSFER_FROM_WITHOUT_DATA_HASH) &&
txCode.includes(SAFE_TRANSFER_FROM_WITHOUT_DATA_HASH) &&
tx.to !== '0x57f1887a8BF19b14fC0dF6Fd9B2acc9Af147eA85') || tx.to !== '0x57f1887a8BF19b14fC0dF6Fd9B2acc9Af147eA85') ||
(isTokenTransfer(tx) && !knownTokens.get(tx.to)) (isTokenTransfer(tx) && !knownTokens?.get(tx.to))
) )
} }
@ -79,14 +72,16 @@ export const getERC20DecimalsAndSymbol = async (
const storedTokenInfo = await getTokenInfos(tokenAddress) const storedTokenInfo = await getTokenInfos(tokenAddress)
if (!storedTokenInfo) { if (!storedTokenInfo) {
const [tokenDecimals, tokenSymbol] = await generateBatchRequests({ const [, tokenDecimals, tokenSymbol] = await generateBatchRequests<
abi: ALTERNATIVE_TOKEN_ABI, [undefined, string | undefined, string | undefined]
>({
abi: ALTERNATIVE_TOKEN_ABI as AbiItem[],
address: tokenAddress, address: tokenAddress,
methods: ['decimals', 'symbol'], methods: ['decimals', 'symbol'],
}) })
return { decimals: Number(tokenDecimals), symbol: tokenSymbol } return { decimals: Number(tokenDecimals), symbol: tokenSymbol ?? 'UNKNOWN' }
} }
return { decimals: storedTokenInfo.decimals as number, symbol: storedTokenInfo.symbol } return { decimals: Number(storedTokenInfo.decimals), symbol: storedTokenInfo.symbol }
} catch (err) { } catch (err) {
console.error(`Failed to retrieve token info for ERC20 token ${tokenAddress}`) console.error(`Failed to retrieve token info for ERC20 token ${tokenAddress}`)
} }
@ -96,8 +91,8 @@ export const getERC20DecimalsAndSymbol = async (
export const isSendERC20Transaction = async ( export const isSendERC20Transaction = async (
tx: TxServiceModel, tx: TxServiceModel,
txCode: string | null, txCode?: string,
knownTokens: Map<string, Token>, knownTokens?: TokenState,
): Promise<boolean> => { ): Promise<boolean> => {
let isSendTokenTx = !isSendERC721Transaction(tx, txCode, knownTokens) && isTokenTransfer(tx) let isSendTokenTx = !isSendERC721Transaction(tx, txCode, knownTokens) && isTokenTransfer(tx)

View File

@ -1,24 +1,12 @@
import Web3 from 'web3' import Web3 from 'web3'
import { provider as Provider } from 'web3-core'
import { ContentHash } from 'web3-eth-ens'
import { sameAddress } from './ethAddresses' import { sameAddress } from './ethAddresses'
import { EMPTY_DATA } from './ethTransactions' import { EMPTY_DATA } from './ethTransactions'
import { getNetwork } from '../../config'
import { ContentHash } from 'web3-eth-ens'
import { provider as Provider } from 'web3-core'
import { ProviderProps } from './store/model/provider' import { ProviderProps } from './store/model/provider'
import { NODE_ENV } from 'src/utils/constants'
export const ETHEREUM_NETWORK = { import { getRpcServiceUrl } from 'src/config'
MAINNET: 'MAINNET' as const,
MORDEN: 'MORDEN' as const,
ROPSTEN: 'ROPSTEN' as const,
RINKEBY: 'RINKEBY' as const,
GOERLI: 'GOERLI' as const,
KOVAN: 'KOVAN' as const,
UNKNOWN: 'UNKNOWN' as const,
}
export type EthereumNetworks = typeof ETHEREUM_NETWORK[keyof typeof ETHEREUM_NETWORK]
export const WALLET_PROVIDER = { export const WALLET_PROVIDER = {
SAFE: 'SAFE', SAFE: 'SAFE',
@ -38,34 +26,16 @@ export const WALLET_PROVIDER = {
TREZOR: 'TREZOR', TREZOR: 'TREZOR',
} }
export const ETHEREUM_NETWORK_IDS = {
1: ETHEREUM_NETWORK.MAINNET,
2: ETHEREUM_NETWORK.MORDEN,
3: ETHEREUM_NETWORK.ROPSTEN,
4: ETHEREUM_NETWORK.RINKEBY,
5: ETHEREUM_NETWORK.GOERLI,
42: ETHEREUM_NETWORK.KOVAN,
}
export const getEtherScanLink = (type: string, value: string): string => {
const network = getNetwork()
return `https://${
network.toLowerCase() === 'mainnet' ? '' : `${network.toLowerCase()}.`
}etherscan.io/${type}/${value}`
}
export const getInfuraUrl = (): string => {
const isMainnet = process.env.REACT_APP_NETWORK === 'mainnet'
return `https://${isMainnet ? 'mainnet' : 'rinkeby'}.infura.io:443/v3/${process.env.REACT_APP_INFURA_TOKEN}`
}
// With some wallets from web3connect you have to use their provider instance only for signing // With some wallets from web3connect you have to use their provider instance only for signing
// And our own one to fetch data // And our own one to fetch data
export const web3ReadOnly = const httpProviderOptions = {
timeout: 10_000,
}
export const web3ReadOnly = new Web3(
process.env.NODE_ENV !== 'test' process.env.NODE_ENV !== 'test'
? new Web3(new Web3.providers.HttpProvider(getInfuraUrl())) ? new Web3.providers.HttpProvider(getRpcServiceUrl(), httpProviderOptions)
: new Web3(window.web3?.currentProvider || 'ws://localhost:8545') : window.web3?.currentProvider || 'ws://localhost:8545',
)
let web3 = web3ReadOnly let web3 = web3ReadOnly
export const getWeb3 = (): Web3 => web3 export const getWeb3 = (): Web3 => web3
@ -77,7 +47,7 @@ export const resetWeb3 = (): void => {
export const getAccountFrom = async (web3Provider: Web3): Promise<string | null> => { export const getAccountFrom = async (web3Provider: Web3): Promise<string | null> => {
const accounts = await web3Provider.eth.getAccounts() const accounts = await web3Provider.eth.getAccounts()
if (process.env.NODE_ENV === 'test' && window.testAccountIndex) { if (NODE_ENV === 'test' && window.testAccountIndex) {
return accounts[window.testAccountIndex] return accounts[window.testAccountIndex]
} }

View File

@ -2,10 +2,10 @@ import ReactGA from 'react-ga'
import addProvider from './addProvider' import addProvider from './addProvider'
import { getNetwork } from 'src/config' import { getNetworkId, getNetworkInfo } from 'src/config'
import { NOTIFICATIONS, enhanceSnackbarForAction } from 'src/logic/notifications' import { NOTIFICATIONS, enhanceSnackbarForAction } from 'src/logic/notifications'
import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar' import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar'
import { ETHEREUM_NETWORK, ETHEREUM_NETWORK_IDS, getProviderInfo, getWeb3 } from 'src/logic/wallets/getWeb3' import { getProviderInfo, getWeb3 } from 'src/logic/wallets/getWeb3'
import { makeProvider } from 'src/logic/wallets/store/model/provider' import { makeProvider } from 'src/logic/wallets/store/model/provider'
import { updateStoredTransactionsStatus } from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers' import { updateStoredTransactionsStatus } from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers'
import { Dispatch } from 'redux' import { Dispatch } from 'redux'
@ -24,12 +24,13 @@ const handleProviderNotification = (provider, dispatch) => {
return return
} }
if (ETHEREUM_NETWORK_IDS[network] !== getNetwork()) { if (network !== getNetworkId()) {
dispatch(enqueueSnackbar(NOTIFICATIONS.WRONG_NETWORK_MSG)) dispatch(enqueueSnackbar(NOTIFICATIONS.WRONG_NETWORK_MSG))
return return
} }
if (ETHEREUM_NETWORK.RINKEBY === getNetwork()) {
dispatch(enqueueSnackbar(enhanceSnackbarForAction(NOTIFICATIONS.RINKEBY_VERSION_MSG))) if (getNetworkInfo().isTestNet) {
dispatch(enqueueSnackbar(enhanceSnackbarForAction(NOTIFICATIONS.TESTNET_VERSION_MSG)))
} }
if (available) { if (available) {

View File

@ -1,11 +1,13 @@
import { Record, RecordOf } from 'immutable' import { Record, RecordOf } from 'immutable'
import { ETHEREUM_NETWORK } from 'src/config/networks/network.d'
export type ProviderProps = { export type ProviderProps = {
name: string name: string
loaded: boolean loaded: boolean
available: boolean available: boolean
account: string account: string
network: number network: ETHEREUM_NETWORK
smartContractWallet: boolean smartContractWallet: boolean
hardwareWallet: boolean hardwareWallet: boolean
} }
@ -15,7 +17,7 @@ export const makeProvider = Record<ProviderProps>({
loaded: false, loaded: false,
available: false, available: false,
account: '', account: '',
network: 0, network: ETHEREUM_NETWORK.UNKNOWN,
smartContractWallet: false, smartContractWallet: false,
hardwareWallet: false, hardwareWallet: false,
}) })

View File

@ -1,6 +1,6 @@
import { createSelector } from 'reselect' import { createSelector } from 'reselect'
import { ETHEREUM_NETWORK, ETHEREUM_NETWORK_IDS, EthereumNetworks } from 'src/logic/wallets/getWeb3' import { ETHEREUM_NETWORK } from 'src/config/networks/network.d'
import { PROVIDER_REDUCER_ID, ProviderState } from 'src/logic/wallets/store/reducer/provider' import { PROVIDER_REDUCER_ID, ProviderState } from 'src/logic/wallets/store/reducer/provider'
import { AppReduxState } from 'src/store' import { AppReduxState } from 'src/store'
@ -18,9 +18,10 @@ export const providerNameSelector = createSelector(providerSelector, (provider:
export const networkSelector = createSelector( export const networkSelector = createSelector(
providerSelector, providerSelector,
(provider: ProviderState): EthereumNetworks => { (provider: ProviderState): ETHEREUM_NETWORK => {
const networkId = provider.get('network') const networkId = provider.get('network')
return ETHEREUM_NETWORK_IDS[networkId] || ETHEREUM_NETWORK.UNKNOWN
return networkId ?? ETHEREUM_NETWORK.UNKNOWN
}, },
) )

View File

@ -1,19 +1,25 @@
import { getInfuraUrl } from '../getWeb3' import { WalletInitOptions } from 'bnc-onboard/dist/src/interfaces'
const isMainnet = process.env.REACT_APP_NETWORK === 'mainnet' import { getNetworkId, getRpcServiceUrl } from 'src/config'
import { ETHEREUM_NETWORK } from 'src/config/networks/network.d'
import { FORTMATIC_KEY, PORTIS_ID } from 'src/utils/constants'
const PORTIS_DAPP_ID = isMainnet ? process.env.REACT_APP_PORTIS_ID : '852b763d-f28b-4463-80cb-846d7ec5806b' const networkId = getNetworkId()
// const SQUARELINK_CLIENT_ID = isMainnet ? process.env.REACT_APP_SQUARELINK_ID : '46ce08fe50913cfa1b78' const PORTIS_DAPP_ID = PORTIS_ID[networkId] ?? PORTIS_ID[ETHEREUM_NETWORK.RINKEBY]
const FORTMATIC_API_KEY = isMainnet ? process.env.REACT_APP_FORTMATIC_KEY : 'pk_test_CAD437AA29BE0A40' const FORTMATIC_API_KEY = FORTMATIC_KEY[networkId] ?? FORTMATIC_KEY[ETHEREUM_NETWORK.RINKEBY]
const infuraUrl = getInfuraUrl() type Wallet = WalletInitOptions & {
desktop: boolean
}
const wallets = [ const rpcUrl = getRpcServiceUrl()
const wallets: Wallet[] = [
{ walletName: 'metamask', preferred: true, desktop: false }, { walletName: 'metamask', preferred: true, desktop: false },
{ {
walletName: 'walletConnect', walletName: 'walletConnect',
preferred: true, preferred: true,
infuraKey: process.env.REACT_APP_INFURA_TOKEN, // as stated in the documentation, `infuraKey` is not mandatory if rpc is provided
rpc: { [networkId]: rpcUrl },
desktop: true, desktop: true,
bridge: 'https://safe-walletconnect.gnosis.io/', bridge: 'https://safe-walletconnect.gnosis.io/',
}, },
@ -23,13 +29,13 @@ const wallets = [
preferred: true, preferred: true,
email: 'safe@gnosis.io', email: 'safe@gnosis.io',
desktop: true, desktop: true,
rpcUrl: infuraUrl, rpcUrl,
}, },
{ {
walletName: 'ledger', walletName: 'ledger',
desktop: true, desktop: true,
preferred: true, preferred: true,
rpcUrl: infuraUrl, rpcUrl,
LedgerTransport: (window as any).TransportNodeHid, LedgerTransport: (window as any).TransportNodeHid,
}, },
{ walletName: 'trust', preferred: true, desktop: false }, { walletName: 'trust', preferred: true, desktop: false },
@ -48,16 +54,18 @@ const wallets = [
{ walletName: 'torus', desktop: true }, { walletName: 'torus', desktop: true },
{ walletName: 'unilogin', desktop: true }, { walletName: 'unilogin', desktop: true },
{ walletName: 'coinbase', desktop: false }, { walletName: 'coinbase', desktop: false },
{ walletName: 'walletLink', rpcUrl: infuraUrl, desktop: false }, { walletName: 'walletLink', rpcUrl, desktop: false },
{ walletName: 'opera', desktop: false }, { walletName: 'opera', desktop: false },
{ walletName: 'operaTouch', desktop: false }, { walletName: 'operaTouch', desktop: false },
] ]
export const getSupportedWallets = () => { export const getSupportedWallets = (): WalletInitOptions[] => {
const { isDesktop } = window as any const { isDesktop } = window as any
/* eslint-disable no-unused-vars */ /* eslint-disable no-unused-vars */
if (isDesktop) return wallets.filter((wallet) => wallet.desktop).map(({ desktop, ...rest }) => rest) if (isDesktop) {
return wallets.filter((wallet) => wallet.desktop).map(({ desktop, ...rest }) => rest)
}
return wallets.map(({ desktop, ...rest }) => rest) return wallets.map(({ desktop, ...rest }) => rest)
} }

View File

@ -43,7 +43,7 @@ const useStyles = makeStyles({
}) })
export const SAFE_INSTANCE_ERROR = 'Address given is not a Safe instance' export const SAFE_INSTANCE_ERROR = 'Address given is not a Safe instance'
export const SAFE_MASTERCOPY_ERROR = 'Mastercopy used by this Safe is not the same' export const SAFE_MASTERCOPY_ERROR = 'Address is not a Safe or mastercopy is not supported'
// In case of an error here, it will be swallowed by final-form // In case of an error here, it will be swallowed by final-form
// So if you're experiencing any strang behaviours like freeze or hanging // So if you're experiencing any strang behaviours like freeze or hanging
@ -73,7 +73,7 @@ export const safeFieldsValidation = async (values): Promise<Record<string, strin
`0x${proxyAddressFromStorage.substr(proxyAddressFromStorage.length - 40)}`, `0x${proxyAddressFromStorage.substr(proxyAddressFromStorage.length - 40)}`,
) )
const safeMaster = await getSafeMasterContract() const safeMaster = await getSafeMasterContract()
const masterCopy = safeMaster.address const masterCopy = safeMaster.options.address
const sameMasterCopy = const sameMasterCopy =
checksummedProxyAddress === masterCopy || checksummedProxyAddress === SAFE_MASTER_COPY_ADDRESS_V10 checksummedProxyAddress === masterCopy || checksummedProxyAddress === SAFE_MASTER_COPY_ADDRESS_V10
if (!sameMasterCopy) { if (!sameMasterCopy) {

View File

@ -1,28 +1,28 @@
import TableContainer from '@material-ui/core/TableContainer'
import { makeStyles } from '@material-ui/core/styles' import { makeStyles } from '@material-ui/core/styles'
import TableContainer from '@material-ui/core/TableContainer'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { useSelector } from 'react-redux'
import CopyBtn from 'src/components/CopyBtn' import CopyBtn from 'src/components/CopyBtn'
import EtherscanBtn from 'src/components/EtherscanBtn' import EtherscanBtn from 'src/components/EtherscanBtn'
import Identicon from 'src/components/Identicon'
import OpenPaper from 'src/components/Stepper/OpenPaper'
import Field from 'src/components/forms/Field' import Field from 'src/components/forms/Field'
import TextField from 'src/components/forms/TextField' import TextField from 'src/components/forms/TextField'
import { composeValidators, minMaxLength, required } from 'src/components/forms/validator' import { composeValidators, minMaxLength, required } from 'src/components/forms/validator'
import Identicon from 'src/components/Identicon'
import Block from 'src/components/layout/Block' import Block from 'src/components/layout/Block'
import Col from 'src/components/layout/Col' import Col from 'src/components/layout/Col'
import Hairline from 'src/components/layout/Hairline' import Hairline from 'src/components/layout/Hairline'
import Paragraph from 'src/components/layout/Paragraph' import Paragraph from 'src/components/layout/Paragraph'
import Row from 'src/components/layout/Row' import Row from 'src/components/layout/Row'
import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts' import OpenPaper from 'src/components/Stepper/OpenPaper'
import { FIELD_LOAD_ADDRESS, THRESHOLD } from 'src/routes/load/components/fields' import { AddressBookEntry } from 'src/logic/addressBook/model/addressBook'
import { getOwnerAddressBy, getOwnerNameBy } from 'src/routes/open/components/fields'
import { useSelector } from 'react-redux'
import { addressBookSelector } from 'src/logic/addressBook/store/selectors' import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
import { formatAddressListToAddressBookNames } from 'src/logic/addressBook/utils' import { formatAddressListToAddressBookNames } from 'src/logic/addressBook/utils'
import { AddressBookEntry } from 'src/logic/addressBook/model/addressBook' import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
import { FIELD_LOAD_ADDRESS, THRESHOLD } from 'src/routes/load/components/fields'
import { getOwnerAddressBy, getOwnerNameBy } from 'src/routes/open/components/fields'
import { styles } from './styles' import { styles } from './styles'
const calculateSafeValues = (owners, threshold, values) => { const calculateSafeValues = (owners, threshold, values) => {
@ -112,7 +112,7 @@ const OwnerListComponent = (props) => {
{address} {address}
</Paragraph> </Paragraph>
<CopyBtn content={address} /> <CopyBtn content={address} />
<EtherscanBtn type="address" value={address} /> <EtherscanBtn value={address} />
</Row> </Row>
</Col> </Col>
</Row> </Row>

View File

@ -5,12 +5,12 @@ import React from 'react'
import CopyBtn from 'src/components/CopyBtn' import CopyBtn from 'src/components/CopyBtn'
import EtherscanBtn from 'src/components/EtherscanBtn' import EtherscanBtn from 'src/components/EtherscanBtn'
import Identicon from 'src/components/Identicon' import Identicon from 'src/components/Identicon'
import OpenPaper from 'src/components/Stepper/OpenPaper'
import Block from 'src/components/layout/Block' import Block from 'src/components/layout/Block'
import Col from 'src/components/layout/Col' import Col from 'src/components/layout/Col'
import Hairline from 'src/components/layout/Hairline' import Hairline from 'src/components/layout/Hairline'
import Paragraph from 'src/components/layout/Paragraph' import Paragraph from 'src/components/layout/Paragraph'
import Row from 'src/components/layout/Row' import Row from 'src/components/layout/Row'
import OpenPaper from 'src/components/Stepper/OpenPaper'
import { shortVersionOf } from 'src/logic/wallets/ethAddresses' import { shortVersionOf } from 'src/logic/wallets/ethAddresses'
import { FIELD_LOAD_ADDRESS, FIELD_LOAD_NAME, THRESHOLD } from 'src/routes/load/components/fields' import { FIELD_LOAD_ADDRESS, FIELD_LOAD_NAME, THRESHOLD } from 'src/routes/load/components/fields'
import { getNumOwnersFrom, getOwnerAddressBy, getOwnerNameBy } from 'src/routes/open/components/fields' import { getNumOwnersFrom, getOwnerAddressBy, getOwnerNameBy } from 'src/routes/open/components/fields'
@ -76,7 +76,7 @@ const ReviewComponent = ({ userAddress, values }: Props): React.ReactElement =>
{shortVersionOf(safeAddress, 4)} {shortVersionOf(safeAddress, 4)}
</Paragraph> </Paragraph>
<CopyBtn content={safeAddress} /> <CopyBtn content={safeAddress} />
<EtherscanBtn type="address" value={safeAddress} /> <EtherscanBtn value={safeAddress} />
</Row> </Row>
</Block> </Block>
<Block margin="lg"> <Block margin="lg">
@ -121,7 +121,7 @@ const ReviewComponent = ({ userAddress, values }: Props): React.ReactElement =>
{address} {address}
</Paragraph> </Paragraph>
<CopyBtn content={address} /> <CopyBtn content={address} />
<EtherscanBtn type="address" value={address} /> <EtherscanBtn value={address} />
</Block> </Block>
</Block> </Block>
</Col> </Col>

Some files were not shown because too many files have changed in this diff Show More