Merge branch 'development' of github.com:gnosis/safe-react into development

This commit is contained in:
Mati Dastugue 2020-10-28 13:41:33 -03:00
commit b1c53f8876
209 changed files with 6360 additions and 4426 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 (branch = release/2.14.0) OR (type = pull_request) OR (tag IS present)
sudo: required sudo: required
dist: bionic dist: bionic
language: node_js language: node_js
@ -10,20 +10,39 @@ 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 AND NOT type = pull_request) OR tag IS present
- env:
- REACT_APP_NETWORK='volta'
- REACT_APP_GOOGLE_ANALYTICS=${REACT_APP_GOOGLE_ANALYTICS_ID_VOLTA}
- STAGING_BUCKET_NAME=${STAGING_VOLTA_BUCKET_NAME}
if: (branch = master AND NOT type = pull_request) OR tag IS present
- env:
- REACT_APP_NETWORK='energy_web_chain'
- REACT_APP_GOOGLE_ANALYTICS=${REACT_APP_GOOGLE_ANALYTICS_ID_EWC}
- STAGING_BUCKET_NAME=${STAGING_EWC_BUCKET_NAME}
if: ((branch = master OR branch = release/2.14.0) AND NOT type = pull_request) OR tag IS present
cache: cache:
yarn: true yarn: true
before_script: before_script:
- if [[ -n "$TRAVIS_TAG" ]]; then export REACT_APP_ENV='production'; fi; - if [[ -n "$TRAVIS_TAG" ]]; then export REACT_APP_ENV='production'; fi;
- if [ $TRAVIS_PULL_REQUEST != "false" ]; then export PUBLIC_URL="/${REACT_APP_NETWORK}/app"; fi;
before_install: before_install:
# Needed to deploy pull request and releases # Needed to deploy pull request and releases
- sudo apt-get update - sudo apt-get update
- sudo apt-get -y install python-pip python-dev libusb-1.0-0-dev - sudo apt-get -y install python-pip python-dev libusb-1.0-0-dev libudev-dev
- pip install awscli --upgrade --user - pip install awscli --upgrade --user
script: script:
- yarn lint:check - yarn lint:check
@ -47,7 +66,7 @@ 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
@ -59,11 +78,24 @@ 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: current/app upload_dir: current/app
region: $AWS_DEFAULT_REGION region: $AWS_DEFAULT_REGION
on: on:
branch: master branch: master
# EWC 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/2.14.0
condition: $REACT_APP_NETWORK = energy_web_chain
# Prepare production deployment # Prepare production deployment
- provider: s3 - provider: s3
bucket: $STAGING_BUCKET_NAME bucket: $STAGING_BUCKET_NAME
@ -71,7 +103,7 @@ deploy:
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

@ -15,11 +15,11 @@ function deploy_pull_request {
REVIEW_FEATURE_FOLDER="$REPO_NAME_ALPHANUMERIC/$PULL_REQUEST_NAME" REVIEW_FEATURE_FOLDER="$REPO_NAME_ALPHANUMERIC/$PULL_REQUEST_NAME"
# Deploy safe-team project # Deploy safe-team project
aws s3 sync build s3://${REVIEW_BUCKET_NAME}/${REVIEW_FEATURE_FOLDER}/app --delete aws s3 sync build s3://${REVIEW_BUCKET_NAME}/${REVIEW_FEATURE_FOLDER}/${REACT_APP_NETWORK}/app --delete
} }
function publish_pull_request_urls_in_github { function publish_pull_request_urls_in_github {
REVIEW_FEATURE_URL="https://$PULL_REQUEST_NAME--$REPO_NAME_ALPHANUMERIC.$REVIEW_ENVIRONMENT_DOMAIN/app" REVIEW_FEATURE_URL="https://$PULL_REQUEST_NAME--$REPO_NAME_ALPHANUMERIC.$REVIEW_ENVIRONMENT_DOMAIN/$REACT_APP_NETWORK/app"
# Using the Issues api instead of the PR api # Using the Issues api instead of the PR api
# Done so because every PR is an issue, and the issues api allows to post general comments, # Done so because every PR is an issue, and the issues api allows to post general comments,

View File

@ -1,6 +1,6 @@
{ {
"name": "safe-react", "name": "safe-react",
"version": "2.12.1", "version": "2.13.1",
"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",
@ -149,6 +148,8 @@
} }
}, },
"resolutions": { "resolutions": {
"@typescript-eslint/eslint-plugin": "^4.5.0",
"@typescript-eslint/parser": "^4.5.0",
"node-gyp": "^5.1.0" "node-gyp": "^5.1.0"
}, },
"browserslist": { "browserslist": {
@ -164,36 +165,38 @@
] ]
}, },
"dependencies": { "dependencies": {
"@gnosis.pm/safe-apps-sdk": "0.4.0", "@gnosis.pm/safe-apps-sdk": "https://github.com/gnosis/safe-apps-sdk.git#3f0689f",
"@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.26",
"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 +205,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.6",
"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.2",
"react-final-form-listeners": "^1.0.2", "react-final-form-listeners": "^1.0.2",
"react-ga": "3.1.2", "react-ga": "3.2.0",
"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 +226,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",
@ -236,43 +237,43 @@
"@storybook/addons": "^5.3.19", "@storybook/addons": "^5.3.19",
"@storybook/preset-create-react-app": "^3.1.4", "@storybook/preset-create-react-app": "^3.1.4",
"@storybook/react": "^5.3.19", "@storybook/react": "^5.3.19",
"@testing-library/jest-dom": "5.11.4", "@testing-library/jest-dom": "5.11.5",
"@testing-library/react": "10.4.9", "@testing-library/react": "10.4.9",
"@typechain/web3-v1": "^1.0.0", "@typechain/web3-v1": "^1.0.0",
"@types/history": "4.6.2", "@types/history": "4.6.2",
"@types/jest": "^26.0.14", "@types/jest": "^26.0.15",
"@types/lodash.memoize": "^4.1.6", "@types/lodash.memoize": "^4.1.6",
"@types/node": "14.11.2", "@types/node": "^14.14.5",
"@types/react": "^16.9.49", "@types/react": "^16.9.54",
"@types/react-dom": "^16.9.6", "@types/react-dom": "^16.9.9",
"@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": "4.6.0",
"@typescript-eslint/parser": "3.9.1", "@typescript-eslint/parser": "4.6.0",
"autoprefixer": "9.8.6", "autoprefixer": "9.8.6",
"cross-env": "^7.0.2", "cross-env": "^7.0.2",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",
"dotenv-expand": "^5.1.0", "dotenv-expand": "^5.1.0",
"electron": "9.3.0", "electron": "9.3.1",
"electron-builder": "22.8.0", "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.14.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.5",
"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.2",
"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",
"react-docgen-typescript-loader": "^3.7.2", "react-docgen-typescript-loader": "^3.7.2",
"typechain": "^2.0.0", "typechain": "^2.0.0",
"typescript": "3.9.7", "typescript": "4.0.5",
"wait-on": "5.2.0" "wait-on": "5.2.0"
} }
} }

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,
@ -88,80 +92,78 @@ function createWindow() {
allowRunningInsecureContent: true, allowRunningInsecureContent: true,
nativeWindowOpen: true, // need to be set in order to display modal nativeWindowOpen: true, // need to be set in order to display modal
}, },
icon: 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

Before

Width:  |  Height:  |  Size: 337 B

After

Width:  |  Height:  |  Size: 337 B

View File

Before

Width:  |  Height:  |  Size: 345 B

After

Width:  |  Height:  |  Size: 345 B

View File

Before

Width:  |  Height:  |  Size: 324 B

After

Width:  |  Height:  |  Size: 324 B

View File

@ -1,52 +1,7 @@
<?xml version="1.0" encoding="iso-8859-1"?> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" <g fill="none" fill-rule="evenodd">
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve"> <rect width="2" height="8" x="9" y="8" fill="#B2B5B2" rx="1"/>
<g> <rect width="2" height="2" x="9" y="4" fill="#B2B5B2" stroke="#B2B5B2" stroke-width=".5" rx="1"/>
<g> <circle cx="10" cy="10" r="9" stroke="#B2B5B2" stroke-width="2"/>
<path d="M256,0C114.497,0,0,114.507,0,256c0,141.503,114.507,256,256,256c141.503,0,256-114.507,256-256
C512,114.497,397.492,0,256,0z M256,472c-119.393,0-216-96.615-216-216c0-119.393,96.615-216,216-216
c119.393,0,216,96.615,216,216C472,375.393,375.384,472,256,472z" fill="#ff0000"/>
</g> </g>
</g>
<g>
<g>
<path d="M256,214.33c-11.046,0-20,8.954-20,20v128.793c0,11.046,8.954,20,20,20s20-8.955,20-20.001V234.33
C276,223.284,267.046,214.33,256,214.33z" fill="#ff0000" />
</g>
</g>
<g>
<g>
<circle cx="256" cy="162.84" r="27" fill="#ff0000"/>
</g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 391 B

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<g>
<g>
<path d="M256,0C114.497,0,0,114.507,0,256c0,141.503,114.507,256,256,256c141.503,0,256-114.507,256-256
C512,114.497,397.492,0,256,0z M256,472c-119.393,0-216-96.615-216-216c0-119.393,96.615-216,216-216
c119.393,0,216,96.615,216,216C472,375.393,375.384,472,256,472z" fill="#ff0000"/>
</g>
</g>
<g>
<g>
<path d="M256,214.33c-11.046,0-20,8.954-20,20v128.793c0,11.046,8.954,20,20,20s20-8.955,20-20.001V234.33
C276,223.284,267.046,214.33,256,214.33z" fill="#ff0000" />
</g>
</g>
<g>
<g>
<circle cx="256" cy="162.84" r="27" fill="#ff0000"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 859 B

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

@ -1,7 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
<g fill="none" fill-rule="evenodd">
<rect width="2" height="8" x="9" y="8" fill="#B2B5B2" rx="1"/>
<rect width="2" height="2" x="9" y="4" fill="#B2B5B2" stroke="#B2B5B2" stroke-width=".5" rx="1"/>
<circle cx="10" cy="10" r="9" stroke="#B2B5B2" stroke-width="2"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 391 B

View File

@ -5,10 +5,10 @@ import { useSelector } from 'react-redux'
import { useRouteMatch, useHistory } from 'react-router-dom' import { useRouteMatch, useHistory } from 'react-router-dom'
import styled from 'styled-components' import styled from 'styled-components'
import AlertIcon from './assets/alert.svg' import AlertIcon from 'src/assets/icons/alert.svg'
import CheckIcon from './assets/check.svg' import CheckIcon from 'src/assets/icons/check.svg'
import ErrorIcon from './assets/error.svg' import ErrorIcon from 'src/assets/icons/error.svg'
import InfoIcon from './assets/info.svg' import InfoIcon from 'src/assets/icons/info.svg'
import AppLayout from 'src/components/AppLayout' import AppLayout from 'src/components/AppLayout'
import SafeListSidebarProvider, { SafeListSidebarContext } from 'src/components/SafeListSidebar' import SafeListSidebarProvider, { SafeListSidebarContext } from 'src/components/SafeListSidebar'
@ -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,8 +125,14 @@ const SafeHeader = ({
</Container> </Container>
) )
} }
const explorerUrl = getExplorerInfo(address)
const networkInfo = getNetworkInfo()
return ( return (
<>
<StyledTextLabel size="sm" networkInfo={networkInfo}>
{networkInfo.label}
</StyledTextLabel>
<Container> <Container>
<IdenticonContainer> <IdenticonContainer>
<FlexSpacer /> <FlexSpacer />
@ -128,7 +149,7 @@ const SafeHeader = ({
<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 : (
@ -147,6 +168,7 @@ const SafeHeader = ({
</Text> </Text>
</StyledButton> </StyledButton>
</Container> </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

@ -16,14 +16,14 @@ const Header = styled.div`
align-items: center; align-items: center;
` `
interface Collapse { interface CollapseProps {
title: React.ReactElement | string title: React.ReactElement | string
description?: React.ReactElement | string description?: React.ReactElement | string
collapseClassName?: string collapseClassName?: string
headerWrapperClassName?: string headerWrapperClassName?: string
} }
const Collapse: React.FC<Collapse> = ({ const Collapse: React.FC<CollapseProps> = ({
children, children,
description = null, description = null,
title, title,

View File

@ -8,10 +8,9 @@ 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 = isMainnet ? process.env.REACT_APP_BLOCKNATIVE_KEY : '7fbb9cee-7e97-4436-8770-8b29a9a8814c'
let lastUsedAddress = '' let lastUsedAddress = ''
let providerName let providerName
@ -19,8 +18,8 @@ let providerName
const wallets = getSupportedWallets() const wallets = getSupportedWallets()
export const onboard = Onboard({ export const onboard = Onboard({
dappId: BLOCKNATIVE_API_KEY, dappId: BLOCKNATIVE_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

@ -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

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 124.7 124.7" style="enable-background:new 0 0 124.7 124.7;" xml:space="preserve">
<style type="text/css">
.st0{fill:#A566FF;}
</style>
<title>ewf_logo</title>
<path class="st0" d="M62.3,0C27.9,0,0,27.9,0,62.3s27.9,62.3,62.3,62.3s62.3-27.9,62.3-62.3l0,0C124.7,27.9,96.8,0,62.3,0z M59.5,93
c-3.4,2.4-7.5,3.8-11.6,3.8c-7.2,0-13.8-3.9-18.8-11.1c-4.5-6.9-7-14.9-7.2-23.1c0-0.3,0.2-0.6,0.5-0.7l4.9-2.2c0.4-0.2,0.8,0,1,0.4
c0,0.1,0.1,0.2,0.1,0.3l0,0c0,8.2,2.2,15.9,6.1,21.6c3.7,5.4,8.5,8.3,13.5,8.3c2.7,0,5.4-0.8,7.6-2.3c0.3-0.2,0.8-0.1,1,0.2
c1,1.3,2,2.5,3.1,3.7C59.8,92.2,59.8,92.7,59.5,93C59.5,93,59.5,93,59.5,93z M95.6,85.7C90.7,92.8,84,96.8,76.8,96.8
S63,92.8,58,85.7c-4.7-6.8-7.3-15.8-7.3-25.3c0-8.8,5.1-15.7,11.5-15.7s11.5,6.9,11.5,15.7c0.1,7.7-1.7,15.2-5.3,22
c-0.2,0.4-0.7,0.5-1,0.3c-0.1,0-0.1-0.1-0.2-0.2c-0.6-0.7-1.2-1.4-1.7-2.1c-0.6-0.9-1.1-1.7-1.6-2.7c-0.1-0.2-0.1-0.5,0-0.7
c2.3-5.3,3.4-11,3.4-16.7c0-4.3-2.2-9.2-5.1-9.2s-5.1,4.9-5.1,9.2c0,8.2,2.2,15.9,6.1,21.6c3.7,5.4,8.5,8.3,13.5,8.3s9.8-3,13.5-8.3
c3.9-5.7,6.1-13.4,6.1-21.6s-2.2-15.9-6.1-21.6c-1.1-1.7-2.5-3.2-4-4.5c-0.3-0.3-0.3-0.8-0.1-1.1c0.1-0.1,0.2-0.2,0.3-0.2l4.9-2.2
c0.3-0.1,0.6-0.1,0.8,0.2c1.3,1.3,2.4,2.7,3.4,4.1c4.7,6.8,7.3,15.8,7.3,25.3S100.3,78.9,95.6,85.7z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 256 255.8" style="enable-background:new 0 0 256 255.8;" xml:space="preserve">
<style type="text/css">
.st0{fill-rule:evenodd;clip-rule:evenodd;fill:#48A9A6;}
.st1{fill-rule:evenodd;clip-rule:evenodd;fill:#FFFFFF;}
</style>
<title>Group 6</title>
<desc>Created with Sketch.</desc>
<g id="Page-1">
<g id="Artboard">
<g id="Group-6">
<path id="Fill-1" class="st0" d="M128,0c70.6,0,128,57.3,128,127.9s-57.4,127.9-128,127.9S0,198.5,0,127.9S57.4,0,128,0z"/>
<polygon id="Fill-2" class="st1" points="62.3,88.6 114.9,88.6 114.9,62.3 62.3,62.3 "/>
<polygon id="Fill-3" class="st1" points="141.1,88.6 193.7,88.6 193.7,62.3 141.1,62.3 "/>
<polygon id="Fill-4" class="st1" points="193.7,141.1 167.4,141.1 167.4,167.4 141.1,167.4 141.1,193.7 193.7,193.7 "/>
<polygon id="Fill-5" class="st1" points="114.9,193.7 114.9,167.4 88.6,167.4 88.6,141.1 62.3,141.1 62.3,193.7 "/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

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,170 @@
import { checksumAddress } from 'src/utils/checksumAddress'; import memoize from 'lodash.memoize'
import networks from 'src/config/networks'
import { EnvironmentSettings, ETHEREUM_NETWORK, NetworkSettings, SafeFeatures, Wallets, GasPriceOracle } 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 {
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,
disabledWallets?: Wallets,
}
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,
disabledWallets: configFile.disabledWallets
}
}
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 getGasPrice = (): number | undefined => getConfig()?.gasPrice
export const getGasPriceOracle = (): GasPriceOracle | undefined => getConfig()?.gasPriceOracle
export const getRpcServiceUrl = (): string => {
const usesInfuraRPC = [ETHEREUM_NETWORK.MAINNET, ETHEREUM_NETWORK.RINKEBY].includes(getNetworkId())
if (usesInfuraRPC) {
return `${getConfig().rpcServiceUrl}/${INFURA_TOKEN}`
} }
return process.env.REACT_APP_ENV === 'production' return getConfig().rpcServiceUrl
? prodConfig }
: stagingConfig
export const getSafeServiceBaseUrl = (safeAddress: string) => `${getTxServiceUrl()}/safes/${safeAddress}`
export const getTokensServiceBaseUrl = () => `${getTxServiceUrl()}/tokens`
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 getNetworkConfigDisabledWallets = (): Wallets => getConfig()?.disabledWallets || []
export const getNetworkInfo = (): NetworkSettings => getConfig().network
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_NETWORK === 'mainnet' if (apiKey) {
? mainnetDevConfig params = { ...params, apiKey }
: devConfig }
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
}
}
} }
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,47 @@
import EwcLogo from 'src/config/assets/token_ewc.svg'
import { EnvironmentSettings, ETHEREUM_NETWORK, NetworkConfig } from 'src/config/networks/network.d'
const baseConfig: EnvironmentSettings = {
txServiceUrl: 'https://safe-transaction.ewc.gnosis.io/api/v1',
safeAppsUrl: 'https://safe-apps.dev.gnosisdev.com',
gasPriceOracle: {
url: 'https://station.energyweb.org',
gasParameter: 'standard',
},
rpcServiceUrl: 'https://rpc.energyweb.org',
networkExplorerName: 'Energy web explorer',
networkExplorerUrl: 'https://explorer.energyweb.org',
networkExplorerApiUrl: 'https://explorer.energyweb.org/api',
}
const mainnet: NetworkConfig = {
environment: {
dev: {
...baseConfig,
},
staging: {
...baseConfig,
safeAppsUrl: 'https://safe-apps.staging.gnosisdev.com',
},
production: {
...baseConfig,
safeAppsUrl: 'https://apps.gnosis-safe.io',
},
},
network: {
id: ETHEREUM_NETWORK.ENERGY_WEB_CHAIN,
backgroundColor: '#A566FF',
textColor: '#ffffff',
label: 'EWC',
isTestNet: false,
nativeCoin: {
address: '0x000',
name: 'Energy web token',
symbol: 'EWT',
decimals: 18,
logoUri: EwcLogo,
},
}
}
export default mainnet

View File

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

View File

@ -0,0 +1,40 @@
import EtherLogo from 'src/config/assets/token_eth.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',
gasPriceOracle: {
url: 'https://ethgasstation.info/json/ethgasAPI.json',
gasParameter: 'average',
},
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,48 @@
import EtherLogo from 'src/config/assets/token_eth.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',
gasPriceOracle: {
url: 'https://ethgasstation.info/json/ethgasAPI.json',
gasParameter: 'average',
},
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

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

@ -0,0 +1,105 @@
// matches src/logic/tokens/store/model/token.ts `TokenProps` type
export enum WALLETS {
METAMASK = 'metamask',
WALLET_CONNECT = 'walletConnect',
TREZOR = 'trezor',
LEDGER = 'ledger',
TRUST = 'trust',
DAPPER = 'dapper',
FORTMATIC = 'fortmatic',
PORTIS = 'portis',
AUTHEREUM = 'authereum',
TORUS = 'torus',
UNILOGIN = 'unilogin',
COINBASE = 'coinbase',
WALLET_LINK = 'walletLink',
OPERA = 'opera',
OPERA_TOUCH = 'operaTouch'
}
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[]
export type Wallets = WALLETS[]
export type GasPriceOracle = {
url: string
// Different gas api providers can use a different name to reflect different gas levels based on tx speed
// For example in ethGasStation for ETHEREUM_MAINNET = safeLow | average | fast
gasParameter: string
}
type GasPrice = {
gasPrice: number
gasPriceOracle?: GasPriceOracle
} | {
gasPrice?: number
// for infura there's a REST API Token required stored in: `REACT_APP_INFURA_TOKEN`
gasPriceOracle: GasPriceOracle
}
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
disabledWallets?: Wallets
environment: SafeEnvironments
}

View File

@ -0,0 +1,48 @@
import EtherLogo from 'src/config/assets/token_eth.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',
gasPriceOracle: {
url: 'https://ethgasstation.info/json/ethgasAPI.json',
gasParameter: 'average',
},
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,47 @@
import EwcLogo from 'src/config/assets/token_ewc.svg'
import { EnvironmentSettings, ETHEREUM_NETWORK, NetworkConfig } from 'src/config/networks/network.d'
const baseConfig: EnvironmentSettings = {
txServiceUrl: 'https://safe-transaction.volta.gnosis.io/api/v1',
safeAppsUrl: 'https://safe-apps.dev.gnosisdev.com',
gasPriceOracle: {
url: 'https://station.energyweb.org',
gasParameter: 'standard',
},
rpcServiceUrl: 'https://volta-rpc.energyweb.org',
networkExplorerName: 'Volta explorer',
networkExplorerUrl: 'https://volta-explorer.energyweb.org',
networkExplorerApiUrl: 'https://volta-explorer.energyweb.org/api',
}
const mainnet: NetworkConfig = {
environment: {
dev: {
...baseConfig,
},
staging: {
...baseConfig,
safeAppsUrl: 'https://safe-apps.staging.gnosisdev.com',
},
production: {
...baseConfig,
safeAppsUrl: 'https://apps.gnosis-safe.io',
},
},
network: {
id: ETHEREUM_NETWORK.VOLTA,
backgroundColor: '#514989',
textColor: '#ffffff',
label: 'Volta',
isTestNet: true,
nativeCoin: {
address: '0x000',
name: 'Energy web token',
symbol: 'EWT',
decimals: 18,
logoUri: EwcLogo,
},
}
}
export default mainnet

View File

@ -0,0 +1,45 @@
import { EnvironmentSettings, ETHEREUM_NETWORK, WALLETS, NetworkConfig } from 'src/config/networks/network.d'
import xDaiLogo from 'src/config/assets/token_xdai.svg'
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: xDaiLogo,
},
},
disabledWallets:[
WALLETS.TREZOR,
WALLETS.LEDGER
]
}
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 { getNetwork } from 'src/config'
import { getConfiguredSource } from 'src/logic/collectibles/sources'
import { addNftAssets, addNftTokens } from 'src/logic/collectibles/store/actions/addCollectibles'
import { Dispatch } from 'redux' import { Dispatch } from 'redux'
const fetchCollectibles = (safeAddress: string) => async (dispatch: Dispatch): Promise<void> => { import { getConfiguredSource } from 'src/logic/collectibles/sources'
import { addNftAssets, addNftTokens } from 'src/logic/collectibles/store/actions/addCollectibles'
export 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))
@ -19,5 +17,3 @@ const fetchCollectibles = (safeAddress: string) => async (dispatch: Dispatch): P
console.log('Error fetching collectibles:', error) console.log('Error fetching collectibles:', error)
} }
} }
export default fetchCollectibles

View File

@ -1,22 +1,33 @@
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) : []
}) })
export const availableNftAssetsAddresses = createSelector(nftTokensSelector, (userNftTokens): string[] => {
return Array.from(new Set(userNftTokens.map((nftToken) => nftToken.assetAddress)))
})
export const activeNftAssetsListSelector = createSelector( export const activeNftAssetsListSelector = createSelector(
nftAssetsListSelector, nftAssetsListSelector,
safeActiveAssetsSelector, safeActiveAssetsSelector,
(assets, activeAssetsList): NFTAsset[] => { availableNftAssetsAddresses,
return assets.filter(({ address }) => activeAssetsList.has(address)) (assets, activeAssetsList, availableNftAssetsAddresses): NFTAsset[] => {
return assets
.filter(({ address }) => activeAssetsList.has(address))
.filter(({ address }) => availableNftAssetsAddresses.includes(address))
}, },
) )

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.warn('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,8 @@
import { aNewStore } from 'src/store'
import fetchTokenCurrenciesBalances from 'src/logic/currencyValues/api/fetchTokenCurrenciesBalances'
import axios from 'axios' import axios from 'axios'
import { getTxServiceHost } from 'src/config'
import { getSafeServiceBaseUrl } from 'src/config'
import { fetchTokenCurrenciesBalances } from 'src/logic/currencyValues/api/fetchTokenCurrenciesBalances'
import { aNewStore } from 'src/store'
jest.mock('axios') jest.mock('axios')
describe('fetchTokenCurrenciesBalances', () => { describe('fetchTokenCurrenciesBalances', () => {
@ -19,26 +20,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 = getSafeServiceBaseUrl(safeAddress)
// @ts-ignore // @ts-ignore
axios.get.mockImplementationOnce(() => Promise.resolve(expectedResult)) axios.get.mockImplementationOnce(() => Promise.resolve(expectedResult))
@ -49,8 +52,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}/balances/usd/?exclude_spam=${excludeSpamTokens}`)
params: { limit: 3000 },
})
}) })
}) })

View File

@ -1,31 +1,32 @@
import axios from 'axios' import axios from 'axios'
import { getExchangeRatesUrl } from 'src/config'
import { AVAILABLE_CURRENCIES } from '../store/model/currencyValues'
import fetchTokenCurrenciesBalances from './fetchTokenCurrenciesBalances'
import BigNumber from 'bignumber.js' import BigNumber from 'bignumber.js'
import { EXCHANGE_RATE_URL } from 'src/utils/constants'
import { fetchTokenCurrenciesBalances } from './fetchTokenCurrenciesBalances'
import { sameString } from 'src/utils/strings'
import { AVAILABLE_CURRENCIES } from '../store/model/currencyValues'
const fetchCurrenciesRates = async ( const fetchCurrenciesRates = async (
baseCurrency: AVAILABLE_CURRENCIES, baseCurrency: string,
targetCurrencyValue: AVAILABLE_CURRENCIES, targetCurrencyValue: string,
safeAddress: string, safeAddress: string,
): Promise<number> => { ): Promise<number> => {
let rate = 0 let rate = 0
if (sameString(targetCurrencyValue, AVAILABLE_CURRENCIES.NETWORK)) {
if (targetCurrencyValue === AVAILABLE_CURRENCIES.ETH) {
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 ${AVAILABLE_CURRENCIES.NETWORK} data from the relayer errored`, error)
} }
return rate return rate
} }
// National currencies
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,22 @@
import axios, { AxiosResponse } from 'axios' import axios, { AxiosResponse } from 'axios'
import { getTxServiceHost } from 'src/config' import { getSafeServiceBaseUrl } from 'src/config'
import { TokenProps } from 'src/logic/tokens/store/model/token' import { TokenProps } from 'src/logic/tokens/store/model/token'
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: string
} }
const fetchTokenCurrenciesBalances = ( export const fetchTokenCurrenciesBalances = (
safeAddress: string, safeAddress: string,
excludeSpamTokens = true, excludeSpamTokens = true,
): Promise<AxiosResponse<BalanceEndpoint[]>> => { ): Promise<AxiosResponse<BalanceEndpoint[]>> => {
const apiUrl = getTxServiceHost() const url = `${getSafeServiceBaseUrl(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

@ -3,7 +3,7 @@ import { setCurrencyRate } from 'src/logic/currencyValues/store/actions/setCurre
import { AVAILABLE_CURRENCIES } from 'src/logic/currencyValues/store/model/currencyValues' import { AVAILABLE_CURRENCIES } from 'src/logic/currencyValues/store/model/currencyValues'
import { Dispatch } from 'redux' import { Dispatch } from 'redux'
const fetchCurrencyRate = (safeAddress: string, selectedCurrency: AVAILABLE_CURRENCIES) => async ( const fetchCurrencyRate = (safeAddress: string, selectedCurrency: string) => async (
dispatch: Dispatch<typeof setCurrencyRate>, dispatch: Dispatch<typeof setCurrencyRate>,
): Promise<void> => { ): Promise<void> => {
if (AVAILABLE_CURRENCIES.USD === selectedCurrency) { if (AVAILABLE_CURRENCIES.USD === selectedCurrency) {

View File

@ -2,20 +2,16 @@ import { createAction } from 'redux-actions'
import { ThunkDispatch } from 'redux-thunk' import { ThunkDispatch } from 'redux-thunk'
import { AnyAction } from 'redux' import { AnyAction } from 'redux'
import { AppReduxState } from 'src/store' import { AppReduxState } from 'src/store'
import { AVAILABLE_CURRENCIES } from '../model/currencyValues'
import fetchCurrencyRate from 'src/logic/currencyValues/store/actions/fetchCurrencyRate' import fetchCurrencyRate from 'src/logic/currencyValues/store/actions/fetchCurrencyRate'
export const SET_CURRENT_CURRENCY = 'SET_CURRENT_CURRENCY' export const SET_CURRENT_CURRENCY = 'SET_CURRENT_CURRENCY'
const setCurrentCurrency = createAction( const setCurrentCurrency = createAction(SET_CURRENT_CURRENCY, (safeAddress: string, selectedCurrency: string) => ({
SET_CURRENT_CURRENCY,
(safeAddress: string, selectedCurrency: AVAILABLE_CURRENCIES) => ({
safeAddress, safeAddress,
selectedCurrency, selectedCurrency,
}), }))
)
export const setSelectedCurrency = (safeAddress: string, selectedCurrency: AVAILABLE_CURRENCIES) => ( export const setSelectedCurrency = (safeAddress: string, selectedCurrency: string) => (
dispatch: ThunkDispatch<AppReduxState, undefined, AnyAction>, dispatch: ThunkDispatch<AppReduxState, undefined, AnyAction>,
): void => { ): void => {
dispatch(setCurrentCurrency(safeAddress, selectedCurrency)) dispatch(setCurrentCurrency(safeAddress, selectedCurrency))

View File

@ -1,41 +1,45 @@
import { List, Record, RecordOf } from 'immutable' import { List, Record, RecordOf } from 'immutable'
export enum AVAILABLE_CURRENCIES { import { getNetworkInfo } from 'src/config'
ETH = 'ETH',
USD = 'USD', const { nativeCoin } = getNetworkInfo()
EUR = 'EUR',
AUD = 'AUD', export const AVAILABLE_CURRENCIES = {
BGN = 'BGN', NETWORK: nativeCoin.symbol.toLocaleUpperCase(),
BRL = 'BRL', USD: 'USD',
CAD = 'CAD', EUR: 'EUR',
CHF = 'CHF', AUD: 'AUD',
CNY = 'CNY', BGN: 'BGN',
CZK = 'CZK', BRL: 'BRL',
DKK = 'DKK', CAD: 'CAD',
GBP = 'GBP', CHF: 'CHF',
HKD = 'HKD', CNY: 'CNY',
HRK = 'HRK', CZK: 'CZK',
HUF = 'HUF', DKK: 'DKK',
IDR = 'IDR', GBP: 'GBP',
ILS = 'ILS', HKD: 'HKD',
INR = 'INR', HRK: 'HRK',
ISK = 'ISK', HUF: 'HUF',
JPY = 'JPY', IDR: 'IDR',
KRW = 'KRW', ILS: 'ILS',
MXN = 'MXN', INR: 'INR',
MYR = 'MYR', ISK: 'ISK',
NOK = 'NOK', JPY: 'JPY',
NZD = 'NZD', KRW: 'KRW',
PHP = 'PHP', MXN: 'MXN',
PLN = 'PLN', MYR: 'MYR',
RON = 'RON', NOK: 'NOK',
RUB = 'RUB', NZD: 'NZD',
SEK = 'SEK', PHP: 'PHP',
SGD = 'SGD', PLN: 'PLN',
THB = 'THB', RON: 'RON',
TRY = 'TRY', RUB: 'RUB',
ZAR = 'ZAR', SEK: 'SEK',
} SGD: 'SGD',
THB: 'THB',
TRY: 'TRY',
ZAR: 'ZAR',
} as const
export type BalanceCurrencyRecord = { export type BalanceCurrencyRecord = {
currencyName?: string currencyName?: string
@ -57,6 +61,6 @@ export type BalanceCurrencyList = List<CurrencyRateValueRecord>
export interface CurrencyRateValue { export interface CurrencyRateValue {
currencyRate?: number currencyRate?: number
selectedCurrency?: AVAILABLE_CURRENCIES selectedCurrency?: string
currencyBalances?: BalanceCurrencyList currencyBalances?: BalanceCurrencyList
} }

View File

@ -1,8 +1,7 @@
import { loadFromStorage, saveToStorage } from 'src/utils/storage' import { loadFromStorage, saveToStorage } from 'src/utils/storage'
import { AVAILABLE_CURRENCIES } from '../model/currencyValues'
const SELECTED_CURRENCY_STORAGE_KEY = 'SELECTED_CURRENCY' const SELECTED_CURRENCY_STORAGE_KEY = 'SELECTED_CURRENCY'
export const saveSelectedCurrency = async (selectedCurrency: AVAILABLE_CURRENCIES): Promise<void> => { export const saveSelectedCurrency = async (selectedCurrency: string): Promise<void> => {
try { try {
await saveToStorage(SELECTED_CURRENCY_STORAGE_KEY, selectedCurrency) await saveToStorage(SELECTED_CURRENCY_STORAGE_KEY, selectedCurrency)
} catch (err) { } catch (err) {
@ -10,6 +9,6 @@ export const saveSelectedCurrency = async (selectedCurrency: AVAILABLE_CURRENCIE
} }
} }
export const loadSelectedCurrency = async (): Promise<AVAILABLE_CURRENCIES | undefined> => { export const loadSelectedCurrency = async (): Promise<string | undefined> => {
return await loadFromStorage(SELECTED_CURRENCY_STORAGE_KEY) return await loadFromStorage(SELECTED_CURRENCY_STORAGE_KEY)
} }

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,17 +0,0 @@
import axios from 'axios'
import { getRelayUrl } from 'src/config/index'
export const estimateTxGas = (safeAddress, to, value, data, operation = 0) => {
const apiUrl = getRelayUrl()
const url = `${apiUrl}/safes/${safeAddress}/transactions/estimate/`
// const estimationValue = isTokenTransfer(tx.data) ? '0' : value.toString(10)
return axios.post(url, {
safe: safeAddress,
to,
data: '0x',
value,
operation,
})
}

View File

@ -1 +0,0 @@
export * from './estimateTxGas'

View File

@ -2,7 +2,7 @@ import { useMemo } from 'react'
import { batch, useDispatch } from 'react-redux' import { batch, useDispatch } from 'react-redux'
import { useLocation } from 'react-router-dom' import { useLocation } from 'react-router-dom'
import fetchCollectibles from 'src/logic/collectibles/store/actions/fetchCollectibles' import { fetchCollectibles } from 'src/logic/collectibles/store/actions/fetchCollectibles'
import { fetchSelectedCurrency } from 'src/logic/currencyValues/store/actions/fetchSelectedCurrency' import { fetchSelectedCurrency } from 'src/logic/currencyValues/store/actions/fetchSelectedCurrency'
import activateAssetsByBalance from 'src/logic/tokens/store/actions/activateAssetsByBalance' import activateAssetsByBalance from 'src/logic/tokens/store/actions/activateAssetsByBalance'
import fetchSafeTokens from 'src/logic/tokens/store/actions/fetchSafeTokens' import fetchSafeTokens from 'src/logic/tokens/store/actions/fetchSafeTokens'

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

@ -1,7 +1,7 @@
import { useEffect, useRef } from 'react' import { useEffect, useRef } from 'react'
import { batch, useDispatch } from 'react-redux' import { batch, useDispatch } from 'react-redux'
import fetchCollectibles from 'src/logic/collectibles/store/actions/fetchCollectibles' import { fetchCollectibles } from 'src/logic/collectibles/store/actions/fetchCollectibles'
import fetchSafeTokens from 'src/logic/tokens/store/actions/fetchSafeTokens' import fetchSafeTokens from 'src/logic/tokens/store/actions/fetchSafeTokens'
import fetchEtherBalance from 'src/logic/safe/store/actions/fetchEtherBalance' import fetchEtherBalance from 'src/logic/safe/store/actions/fetchEtherBalance'
import { checkAndUpdateSafe } from 'src/logic/safe/store/actions/fetchSafe' import { checkAndUpdateSafe } from 'src/logic/safe/store/actions/fetchSafe'

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,8 +1,8 @@
import axios, { AxiosResponse } from 'axios' import axios, { AxiosResponse } from 'axios'
import { getAllTransactionsUriFrom, getTxServiceHost } from 'src/config' import { getSafeServiceBaseUrl } 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 'src/logic/safe/store/models/types/transactions.d'
export type ServiceUriParams = { export type ServiceUriParams = {
safeAddress: string safeAddress: string
@ -21,11 +21,8 @@ type TransactionDTO = {
} }
const getAllTransactionsUri = (safeAddress: string): string => { const getAllTransactionsUri = (safeAddress: string): string => {
const host = getTxServiceHost()
const address = checksumAddress(safeAddress) const address = checksumAddress(safeAddress)
const base = getAllTransactionsUriFrom(address) return `${getSafeServiceBaseUrl(address)}/all-transactions/`
return `${host}${base}`
} }
const fetchAllTransactions = async ( const fetchAllTransactions = async (

View File

@ -16,7 +16,7 @@ import {
saveTxToHistory, saveTxToHistory,
tryOffchainSigning, tryOffchainSigning,
} from 'src/logic/safe/transactions' } from 'src/logic/safe/transactions'
import { estimateSafeTxGas } from 'src/logic/safe/transactions/gasNew' import { estimateSafeTxGas } from 'src/logic/safe/transactions/gas'
import { getCurrentSafeVersion } from 'src/logic/safe/utils/safeVersion' import { getCurrentSafeVersion } from 'src/logic/safe/utils/safeVersion'
import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses' import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses'
import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions' import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions'
@ -42,7 +42,7 @@ import { Transaction, TransactionStatus, TxArgs } from 'src/logic/safe/store/mod
import { AnyAction } from 'redux' import { AnyAction } from 'redux'
import { PayableTx } from 'src/types/contracts/types.d' import { PayableTx } from 'src/types/contracts/types.d'
import { AppReduxState } from 'src/store' import { AppReduxState } from 'src/store'
import { Dispatch } from './types' import { Dispatch, DispatchReturn } from './types'
export const removeTxFromStore = ( export const removeTxFromStore = (
tx: Transaction, tx: Transaction,
@ -107,9 +107,10 @@ interface CreateTransactionArgs {
txData?: string txData?: string
txNonce?: number | string txNonce?: number | string
valueInWei: string valueInWei: string
safeTxGas?: number
} }
type CreateTransactionAction = ThunkAction<Promise<void>, AppReduxState, undefined, AnyAction> type CreateTransactionAction = ThunkAction<Promise<void | string>, AppReduxState, DispatchReturn, AnyAction>
type ConfirmEventHandler = (safeTxHash: string) => void type ConfirmEventHandler = (safeTxHash: string) => void
type ErrorEventHandler = () => void type ErrorEventHandler = () => void
@ -124,10 +125,11 @@ const createTransaction = (
operation = CALL, operation = CALL,
navigateToTransactionsTab = true, navigateToTransactionsTab = true,
origin = null, origin = null,
safeTxGas: safeTxGasArg,
}: CreateTransactionArgs, }: CreateTransactionArgs,
onUserConfirm?: ConfirmEventHandler, onUserConfirm?: ConfirmEventHandler,
onError?: ErrorEventHandler, onError?: ErrorEventHandler,
): CreateTransactionAction => async (dispatch: Dispatch, getState: () => AppReduxState): Promise<void> => { ): CreateTransactionAction => async (dispatch: Dispatch, getState: () => AppReduxState): Promise<DispatchReturn> => {
const state = getState() const state = getState()
if (navigateToTransactionsTab) { if (navigateToTransactionsTab) {
@ -143,7 +145,8 @@ const createTransaction = (
const nonce = await getNewTxNonce(txNonce?.toString(), lastTx, safeInstance) const nonce = await getNewTxNonce(txNonce?.toString(), lastTx, safeInstance)
const isExecution = await shouldExecuteTransaction(safeInstance, nonce, lastTx) const isExecution = await shouldExecuteTransaction(safeInstance, nonce, lastTx)
const safeVersion = await getCurrentSafeVersion(safeInstance) const safeVersion = await getCurrentSafeVersion(safeInstance)
const safeTxGas = await estimateSafeTxGas(safeInstance, safeAddress, txData, to, valueInWei, operation) const safeTxGas =
safeTxGasArg || (await estimateSafeTxGas(safeInstance, safeAddress, txData, to, valueInWei, operation))
// https://docs.gnosis.io/safe/docs/docs5/#pre-validated-signatures // https://docs.gnosis.io/safe/docs/docs5/#pre-validated-signatures
const sigs = `0x000000000000000000000000${from.replace( const sigs = `0x000000000000000000000000${from.replace(

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,13 +12,12 @@ 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 { SafeOwner, SafeRecordProps } from 'src/logic/safe/store/models/safe'
import { Action, Dispatch } from 'redux'
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 'src/logic/safe/store/selectors'
import { getSafeInfo } from 'src/logic/safe/utils/safeInformation'
import { getModules } from 'src/logic/safe/utils/modules'
const buildOwnersFrom = (safeOwners: string[], localSafe?: SafeRecordProps): List<SafeOwner> => { const buildOwnersFrom = (safeOwners: string[], localSafe?: SafeRecordProps): List<SafeOwner> => {
const ownersList = safeOwners.map((ownerAddress) => { const ownersList = safeOwners.map((ownerAddress) => {
@ -40,16 +41,6 @@ const buildOwnersFrom = (safeOwners: string[], localSafe?: SafeRecordProps): Lis
return List(ownersList) return List(ownersList)
} }
const buildModulesLinkedList = (modules: string[] | undefined, nextModule: string): Array<ModulePair> | null => {
if (modules?.length) {
return modules.map((moduleAddress, index, modules) => {
const prevModule = modules[index + 1]
return [moduleAddress, prevModule !== undefined ? prevModule : nextModule]
})
}
return null
}
export const buildSafe = async ( export const buildSafe = async (
safeAdd: string, safeAdd: string,
safeName: string, safeName: string,
@ -58,12 +49,18 @@ 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 [
generateBatchRequests({ [, thresholdStr, nonceStr, currentVersion, remoteOwners = []],
abi: GnosisSafeSol.abi, safeInfo,
localSafe,
ethBalance,
] = await Promise.all([
generateBatchRequests<[undefined, string | undefined, string | undefined, string | undefined, string[]]>({
abi: GnosisSafeSol.abi as AbiItem[],
address: safeAddress, address: safeAddress,
methods: safeParams, methods: safeParams,
}), }),
getSafeInfo(safeAddress),
getLocalSafe(safeAddress), getLocalSafe(safeAddress),
getBalanceInEtherOf(safeAddress), getBalanceInEtherOf(safeAddress),
]) ])
@ -73,6 +70,7 @@ export const buildSafe = async (
const owners = buildOwnersFrom(remoteOwners, localSafe) const owners = buildOwnersFrom(remoteOwners, localSafe)
const needsUpdate = safeNeedsUpdate(currentVersion, latestMasterContractVersion) const needsUpdate = safeNeedsUpdate(currentVersion, latestMasterContractVersion)
const featuresEnabled = enabledFeatures(currentVersion) const featuresEnabled = enabledFeatures(currentVersion)
const modules = await getModules(safeInfo)
return { return {
address: safeAddress, address: safeAddress,
@ -81,48 +79,48 @@ export const buildSafe = async (
owners, owners,
ethBalance, ethBalance,
nonce, nonce,
currentVersion, currentVersion: currentVersion ?? '',
needsUpdate, needsUpdate,
featuresEnabled, featuresEnabled,
balances: Map(), balances: localSafe?.balances || Map(),
latestIncomingTxBlock: 0, latestIncomingTxBlock: 0,
activeAssets: Set(), activeAssets: Set(),
activeTokens: Set(), activeTokens: Set(),
blacklistedAssets: Set(), blacklistedAssets: Set(),
blacklistedTokens: Set(), blacklistedTokens: Set(),
modules: null, modules,
} }
} }
export const checkAndUpdateSafe = (safeAdd: string) => async (dispatch: Dispatch): Promise<void> => { export const checkAndUpdateSafe = (safeAdd: string) => async (dispatch: Dispatch): Promise<void> => {
const safeAddress = checksumAddress(safeAdd) const safeAddress = checksumAddress(safeAdd)
// Check if the owner's safe did change and update them // Check if the owner's safe did change and update them
const safeParams = [ const safeParams = ['getThreshold', 'nonce', 'getOwners']
'getThreshold', const [[, remoteThreshold, remoteNonce, remoteOwners = []], safeInfo, localSafe] = await Promise.all([
'nonce', generateBatchRequests<[undefined, string | undefined, string | undefined, string[]]>({
'getOwners', abi: GnosisSafeSol.abi as AbiItem[],
// 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] },
]
const [[remoteThreshold, remoteNonce, remoteOwners, modules], localSafe] = await Promise.all([
generateBatchRequests({
abi: GnosisSafeSol.abi,
address: safeAddress, address: safeAddress,
methods: safeParams, methods: safeParams,
}), }),
getSafeInfo(safeAddress),
getLocalSafe(safeAddress), getLocalSafe(safeAddress),
]) ])
// Converts from [ { address, ownerName} ] to address array // Converts from [ { address, ownerName} ] to address array
const localOwners = localSafe ? localSafe.owners.map((localOwner) => localOwner.address) : [] const localOwners = localSafe ? localSafe.owners.map((localOwner) => localOwner.address) : []
const modules = await getModules(safeInfo)
dispatch( dispatch(
updateSafe({ updateSafe({
address: safeAddress, address: safeAddress,
name: localSafe?.name, name: localSafe?.name,
modules: buildModulesLinkedList(modules?.array, modules?.next), modules,
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

@ -13,6 +13,8 @@ const getServiceUrl = (txType: string, safeAddress: string): string => {
}[txType](safeAddress) }[txType](safeAddress)
} }
// TODO: Remove this magic
/* eslint-disable */
async function fetchTransactions( async function fetchTransactions(
txType: TransactionTypes.INCOMING, txType: TransactionTypes.INCOMING,
safeAddress: string, safeAddress: string,
@ -28,6 +30,7 @@ async function fetchTransactions(
safeAddress: string, safeAddress: string,
eTag: string | null, eTag: string | null,
): Promise<{ eTag: string | null; results: TxServiceModel[] | IncomingTxServiceModel[] }> { ): Promise<{ eTag: string | null; results: TxServiceModel[] | IncomingTxServiceModel[] }> {
/* eslint-enable */
try { try {
const url = getServiceUrl(txType, safeAddress) const url = getServiceUrl(txType, safeAddress)
const response = await axios.get(url, eTag ? { headers: { 'If-None-Match': eTag } } : undefined) const response = await axios.get(url, eTag ? { headers: { 'If-None-Match': eTag } } : undefined)

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

@ -3,4 +3,6 @@ import { AnyAction } from 'redux'
import { AppReduxState } from 'src/store' import { AppReduxState } from 'src/store'
export type Dispatch = ThunkDispatch<AppReduxState, undefined, AnyAction> export type DispatchReturn = string | undefined
export type Dispatch = ThunkDispatch<AppReduxState, DispatchReturn, AnyAction>

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
@ -13,7 +14,7 @@ export type SafeRecordProps = {
threshold: number threshold: number
ethBalance: string ethBalance: string
owners: List<SafeOwner> owners: List<SafeOwner>
modules: ModulePair[] | null modules?: ModulePair[] | null
activeTokens: Set<string> activeTokens: Set<string>
activeAssets: Set<string> activeAssets: Set<string>
blacklistedTokens: Set<string> blacklistedTokens: Set<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>({
@ -38,7 +39,7 @@ const makeSafe = Record<SafeRecordProps>({
activeAssets: Set(), activeAssets: Set(),
blacklistedTokens: Set(), blacklistedTokens: Set(),
blacklistedAssets: Set(), blacklistedAssets: Set(),
balances: Map({}), balances: Map(),
nonce: 0, nonce: 0,
latestIncomingTxBlock: 0, latestIncomingTxBlock: 0,
recurringUser: undefined, recurringUser: undefined,

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'
@ -54,8 +53,9 @@ const updateSafeProps = (prevSafe, safe) => {
// We check each safe property sent in action.payload // We check each safe property sent in action.payload
safeProperties.forEach((key) => { safeProperties.forEach((key) => {
if (safe[key] && typeof safe[key] === 'object') { if (safe[key] && typeof safe[key] === 'object') {
if (safe[key].length >= 0) { if (safe[key].length >= 0 || Map.isMap(safe[key])) {
// If type is array we update the array // If type is array we replace it
// If type is Immutable Map we replace it
record.update(key, () => safe[key]) record.update(key, () => safe[key])
} else if (safe[key].size >= 0) { } else if (safe[key].size >= 0) {
// If type is Immutable List we replace current List // If type is Immutable List we replace current List
@ -99,19 +99,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
@ -121,7 +109,7 @@ export default handleActions(
return state.updateIn( return state.updateIn(
['safes', safe.address], ['safes', safe.address],
makeSafe({ name: 'LOADED SAFE', address: safe.address }), makeSafe({ name: safe?.name || 'LOADED SAFE', address: safe.address }),
(prevSafe) => updateSafeProps(prevSafe, safe), (prevSafe) => updateSafeProps(prevSafe, safe),
) )
}, },

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

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