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"
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
REACT_APP_GOOGLE_ANALYTICS=
REACT_APP_INFURA_TOKEN=
REACT_APP_IPFS_GATEWAY=https://ipfs.io/ipfs
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
dist: bionic
language: node_js
@ -10,20 +10,39 @@ matrix:
include:
- env:
- REACT_APP_NETWORK='mainnet'
- REACT_APP_GOOGLE_ANALYTICS=${REACT_APP_GOOGLE_ANALYTICS_ID_MAINNET}
- STAGING_BUCKET_NAME=${STAGING_MAINNET_BUCKET_NAME}
- REACT_APP_GNOSIS_APPS_URL=${REACT_APP_GNOSIS_APPS_URL_PROD}
if: (branch = master AND NOT type = pull_request) OR tag IS present
- env:
- 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}
- 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:
yarn: true
before_script:
- 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:
# Needed to deploy pull request and releases
- 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
script:
- yarn lint:check
@ -47,7 +66,7 @@ deploy:
secret_access_key: $AWS_SECRET_ACCESS_KEY
skip_cleanup: true
local_dir: build
upload-dir: app
upload_dir: app
region: $AWS_DEFAULT_REGION
on:
branch: development
@ -59,11 +78,24 @@ deploy:
secret_access_key: $AWS_SECRET_ACCESS_KEY
skip_cleanup: true
local_dir: build
upload-dir: current/app
upload_dir: current/app
region: $AWS_DEFAULT_REGION
on:
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
- provider: s3
bucket: $STAGING_BUCKET_NAME
@ -71,7 +103,7 @@ deploy:
access_key_id: $AWS_ACCESS_KEY_ID
skip_cleanup: true
local_dir: build
upload-dir: releases/$TRAVIS_TAG
upload_dir: releases/$TRAVIS_TAG
region: $AWS_DEFAULT_REGION
on:
tags: true

View File

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

View File

@ -15,11 +15,11 @@ function deploy_pull_request {
REVIEW_FEATURE_FOLDER="$REPO_NAME_ALPHANUMERIC/$PULL_REQUEST_NAME"
# 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 {
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
# 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",
"version": "2.12.1",
"version": "2.13.1",
"description": "Allowing crypto users manage funds in a safer way",
"website": "https://github.com/gnosis/safe-react#readme",
"bugs": {
@ -61,8 +61,7 @@
"src/**/*.{js,jsx,ts,tsx}",
"!src/**/*.{.test.*}",
"!src/**/test/**/*",
"!src/**/assets/**",
"!src/config/**/*"
"!src/**/assets/**"
]
},
"productName": "Safe Multisig",
@ -149,6 +148,8 @@
}
},
"resolutions": {
"@typescript-eslint/eslint-plugin": "^4.5.0",
"@typescript-eslint/parser": "^4.5.0",
"node-gyp": "^5.1.0"
},
"browserslist": {
@ -164,36 +165,38 @@
]
},
"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-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",
"@ledgerhq/hw-transport-node-hid": "5.22.0",
"@ledgerhq/hw-transport-node-hid": "5.26.0",
"@material-ui/core": "4.11.0",
"@material-ui/icons": "4.9.1",
"@material-ui/lab": "4.0.0-alpha.56",
"@openzeppelin/contracts": "3.1.0",
"@truffle/contract": "4.2.26",
"async-sema": "^3.1.0",
"axios": "0.20.0",
"bignumber.js": "9.0.0",
"bnc-onboard": "1.13.1",
"bignumber.js": "9.0.1",
"bnc-onboard": "1.13.2",
"classnames": "^2.2.6",
"concurrently": "^5.3.0",
"connected-react-router": "6.8.0",
"coveralls": "^3.1.0",
"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-log": "4.2.4",
"electron-settings": "^4.0.2",
"electron-updater": "4.3.4",
"electron-updater": "4.3.5",
"eth-sig-util": "^2.5.3",
"ethereum-blockies-base64": "^1.0.2",
"ethereumjs-abi": "0.6.8",
"exponential-backoff": "^3.1.0",
"express": "^4.17.1",
"final-form": "^4.20.1",
"final-form-calculate": "^1.3.1",
"final-form-calculate": "^1.3.2",
"history": "4.10.1",
"immortal-db": "^1.1.0",
"immutable": "^4.0.0-rc.12",
@ -202,16 +205,15 @@
"lodash.memoize": "^4.1.2",
"material-ui-search-bar": "^1.0.0",
"notistack": "https://github.com/gnosis/notistack.git#v0.9.4",
"open": "^7.2.0",
"polished": "3.6.7",
"qrcode.react": "1.0.0",
"query-string": "6.13.1",
"query-string": "6.13.6",
"react": "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-ga": "3.1.2",
"react-hot-loader": "4.12.21",
"react-ga": "3.2.0",
"react-hot-loader": "4.13.0",
"react-qr-reader": "^2.2.1",
"react-redux": "7.2.1",
"react-router-dom": "5.2.0",
@ -224,7 +226,6 @@
"reselect": "^4.0.0",
"semver": "7.3.2",
"styled-components": "^5.2.0",
"truffle-contract": "4.0.31",
"web3": "1.2.9",
"web3-core": "^1.2.11",
"web3-eth-contract": "^1.2.11",
@ -236,43 +237,43 @@
"@storybook/addons": "^5.3.19",
"@storybook/preset-create-react-app": "^3.1.4",
"@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",
"@typechain/web3-v1": "^1.0.0",
"@types/history": "4.6.2",
"@types/jest": "^26.0.14",
"@types/jest": "^26.0.15",
"@types/lodash.memoize": "^4.1.6",
"@types/node": "14.11.2",
"@types/react": "^16.9.49",
"@types/react-dom": "^16.9.6",
"@types/node": "^14.14.5",
"@types/react": "^16.9.54",
"@types/react-dom": "^16.9.9",
"@types/react-redux": "^7.1.9",
"@types/react-router-dom": "^5.1.5",
"@types/styled-components": "^5.1.3",
"@typescript-eslint/eslint-plugin": "3.9.1",
"@typescript-eslint/parser": "3.9.1",
"@types/react-router-dom": "^5.1.6",
"@types/styled-components": "^5.1.4",
"@typescript-eslint/eslint-plugin": "4.6.0",
"@typescript-eslint/parser": "4.6.0",
"autoprefixer": "9.8.6",
"cross-env": "^7.0.2",
"dotenv": "^8.2.0",
"dotenv-expand": "^5.1.0",
"electron": "9.3.0",
"electron-builder": "22.8.0",
"electron": "9.3.1",
"electron-builder": "22.8.1",
"electron-notarize": "1.0.0",
"eslint": "6.8.0",
"eslint-config-prettier": "6.11.0",
"eslint-plugin-import": "2.22.0",
"eslint-config-prettier": "6.14.0",
"eslint-plugin-import": "2.22.1",
"eslint-plugin-jsx-a11y": "^6.3.1",
"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",
"ethereumjs-abi": "0.6.8",
"husky": "^4.2.5",
"lint-staged": "10.4.0",
"husky": "^4.3.0",
"lint-staged": "^10.4.2",
"node-sass": "^4.14.1",
"prettier": "2.1.2",
"react-app-rewired": "^2.1.6",
"react-docgen-typescript-loader": "^3.7.2",
"typechain": "^2.0.0",
"typescript": "3.9.7",
"typescript": "4.0.5",
"wait-on": "5.2.0"
}
}

View File

@ -1,84 +1,88 @@
const electron = require("electron");
const express = require('express');
const open = require('open');
const log = require('electron-log');
const fs = require('fs');
const Menu = electron.Menu;
const https = require('https');
const autoUpdater = require('./auto-updater');
const electron = require('electron')
const express = require('express')
const log = require('electron-log')
const fs = require('fs')
const Menu = electron.Menu
const https = require('https')
const detect = require('detect-port')
const autoUpdater = require('./auto-updater')
const app = electron.app;
const session = electron.session;
const BrowserWindow = electron.BrowserWindow;
const { app, session, BrowserWindow, shell } = electron
const path = require("path");
const isDev = require("electron-is-dev");
const path = require('path')
const isDev = require('electron-is-dev')
const options = {
key: fs.readFileSync(path.join(__dirname, './ssl/server.key')),
cert: fs.readFileSync(path.join(__dirname, './ssl/server.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);
ca: fs.readFileSync(path.join(__dirname, './ssl/rootCA.crt')),
}
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 display = electron.screen.getPrimaryDisplay();
let width = display.bounds.width;
let height = display.bounds.height;
let mainWindow
function getOpenedWindow(url, options) {
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
const filter = {
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});
});
urls: ['http://127.0.0.1:21325/*'],
}
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({
width:350,
height:700,
width: 350,
height: 700,
x: width - 1300,
parent:mainWindow,
parent: mainWindow,
y: height - (process.platform === 'win32' ? 750 : 200),
webContents: options.webContents, // use existing webContents if provided
fullscreen: false,
show: false,
});
win.webContents.on('new-window', function(event, url){
if(url.includes('trezor') && url.includes('bridge'))
open(url);
});
win.once('ready-to-show', () => win.show());
})
win.webContents.on('new-window', function (event, url) {
if (url.includes('trezor') && url.includes('bridge')) shell.openExternal(url)
})
win.once('ready-to-show', () => win.show())
if(!options.webPreferences){
win.loadURL(url);
if (!options.webPreferences) {
win.loadURL(url)
}
return win
}
return null;
return null
}
function createWindow() {
function createWindow(port = DEFAULT_PORT) {
mainWindow = new BrowserWindow({
show: false,
width: 1024,
@ -88,80 +92,78 @@ function createWindow() {
allowRunningInsecureContent: true,
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.show();
});
mainWindow.show()
})
mainWindow.loadURL(
isDev
? "http://localhost:3000"
: `https://localhost:${PORT}`
)
mainWindow.loadURL(isDev ? 'http://localhost:3000' : `https://localhost:${port}`)
if (isDev) {
// Open the DevTools.
mainWindow.webContents.openDevTools();
mainWindow.webContents.openDevTools()
//BrowserWindow.addDevToolsExtension('<location to your react chrome extension>');
}
mainWindow.setMenu(null);
mainWindow.setMenuBarVisibility(false);
mainWindow.setMenu(null)
mainWindow.setMenuBarVisibility(false)
mainWindow.webContents.on('new-window', function(event, url, frameName, disposition, options){
event.preventDefault();
const win = getOpenedWindow(url,options);
if(win){
win.once('ready-to-show', () => win.show());
mainWindow.webContents.on('new-window', function (event, url, frameName, disposition, options) {
event.preventDefault()
const win = getOpenedWindow(url, options)
if (win) {
win.once('ready-to-show', () => win.show())
if(!options.webPreferences){
win.loadURL(url);
if (!options.webPreferences) {
win.loadURL(url)
}
event.newGuest = win
} else open(url);
});
} else shell.openExternal(url)
})
mainWindow.webContents.on('did-finish-load', () => {
autoUpdater.init(mainWindow);
});
autoUpdater.init(mainWindow)
})
mainWindow.webContents.on('crashed', (event) => {
log.info(`App Crashed: ${event}`);
mainWindow.reload();
});
log.info(`App Crashed: ${event}`)
mainWindow.reload()
})
mainWindow.on("closed", () => (mainWindow = null));
mainWindow.on('closed', () => (mainWindow = null))
}
process.on('uncaughtException',function(error){
log.error(error);
});
process.on('uncaughtException', function (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
// 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.on("ready", () =>{
app.commandLine.appendSwitch('ignore-certificate-errors')
app.on('ready', async () => {
// Hide the menu
Menu.setApplicationMenu(null);
if(!isDev) createServer();
createWindow();
});
Menu.setApplicationMenu(null)
let usedPort = DEFAULT_PORT
if (!isDev) usedPort = await createServer()
createWindow(usedPort)
})
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
app.quit();
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
});
})
app.on("activate", () => {
app.on('activate', () => {
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 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"/>
<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>
</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>

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

View File

@ -2,7 +2,7 @@ import IconButton from '@material-ui/core/IconButton'
import { createStyles, makeStyles } from '@material-ui/core/styles'
import Close from '@material-ui/icons/Close'
import QRCode from 'qrcode.react'
import * as React from 'react'
import React, { ReactElement } from 'react'
import CopyBtn from 'src/components/CopyBtn'
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 Paragraph from 'src/components/layout/Paragraph'
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 { getNetworkInfo } from 'src/config'
const networkInfo = getNetworkInfo()
const useStyles = makeStyles(
createStyles({
heading: {
@ -35,6 +37,12 @@ const useStyles = makeStyles(
borderRadius: '6px',
border: `1px solid ${secondaryText}`,
},
networkInfo: {
backgroundColor: `${networkInfo?.backgroundColor ?? border}`,
color: `${networkInfo?.textColor ?? fontColor}`,
padding: md,
marginBottom: 0,
},
annotation: {
margin: lg,
marginBottom: 0,
@ -79,7 +87,7 @@ type Props = {
safeName: string
}
const ReceiveModal = ({ onClose, safeAddress, safeName }: Props) => {
const ReceiveModal = ({ onClose, safeAddress, safeName }: Props): ReactElement => {
const classes = useStyles()
return (
@ -93,9 +101,12 @@ const ReceiveModal = ({ onClose, safeAddress, safeName }: Props) => {
</IconButton>
</Row>
<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">
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!
This is the address of your Safe. Deposit funds by scanning the QR code or copying the address below. Only send{' '}
{networkInfo.nativeCoin.name} and ERC-20 tokens to this address!
</Paragraph>
<Col layout="column" middle="xs">
<Paragraph className={classes.safeName} noMargin size="lg" weight="bold">
@ -115,7 +126,7 @@ const ReceiveModal = ({ onClose, safeAddress, safeName }: Props) => {
{safeAddress}
</Paragraph>
<CopyBtn content={safeAddress} />
<EtherscanBtn type="address" value={safeAddress} />
<EtherscanBtn value={safeAddress} />
</Block>
</Col>
<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 styled from 'styled-components'
import AlertIcon from './assets/alert.svg'
import CheckIcon from './assets/check.svg'
import ErrorIcon from './assets/error.svg'
import InfoIcon from './assets/info.svg'
import AlertIcon from 'src/assets/icons/alert.svg'
import CheckIcon from 'src/assets/icons/check.svg'
import ErrorIcon from 'src/assets/icons/error.svg'
import InfoIcon from 'src/assets/icons/info.svg'
import AppLayout from 'src/components/AppLayout'
import SafeListSidebarProvider, { SafeListSidebarContext } from 'src/components/SafeListSidebar'
@ -16,8 +16,8 @@ import CookiesBanner from 'src/components/CookiesBanner'
import Notifier from 'src/components/Notifier'
import Backdrop from 'src/components/layout/Backdrop'
import Img from 'src/components/layout/Img'
import { getNetwork } from 'src/config'
import { ETHEREUM_NETWORK } from 'src/logic/wallets/getWeb3'
import { getNetworkId } from 'src/config'
import { ETHEREUM_NETWORK } from 'src/config/networks/network.d'
import { networkSelector } from 'src/logic/wallets/store/selectors'
import { SAFELIST_ADDRESS, WELCOME_ADDRESS } from 'src/routes/routes'
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 { useSafeScheduledUpdates } from 'src/logic/safe/hooks/useSafeScheduledUpdates'
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 { grantedSelector } from 'src/routes/safe/container/selector'
import Receive from './ReceiveModal'
import ReceiveModal from './ReceiveModal'
import { useSidebarItems } from 'src/components/AppLayout/Sidebar/useSidebarItems'
const notificationStyles = {
@ -46,6 +46,12 @@ const notificationStyles = {
info: {
background: '#fff',
},
receiveModal: {
height: 'auto',
maxWidth: 'calc(100% - 30px)',
minHeight: '544px',
overflow: 'hidden',
},
}
const Frame = styled.div`
@ -55,7 +61,7 @@ const Frame = styled.div`
max-width: 100%;
`
const desiredNetwork = getNetwork()
const desiredNetwork = getNetworkId()
const useStyles = makeStyles(notificationStyles)
@ -67,7 +73,7 @@ const App: React.FC = ({ children }) => {
const matchSafe = useRouteMatch({ path: `${SAFELIST_ADDRESS}`, strict: false })
const history = useHistory()
const safeAddress = useSelector(safeParamAddressFromStateSelector)
const safeName = useSelector(safeNameSelector)
const safeName = useSelector(safeNameSelector) ?? ''
const { safeActionsState, onShow, onHide, showSendFunds, hideSendFunds } = useSafeActions()
const currentSafeBalance = useSelector(safeFiatBalancesTotalSelector)
const currentCurrency = useSelector(currentCurrencySelector)
@ -77,7 +83,7 @@ const App: React.FC = ({ children }) => {
useLoadSafe(safeAddress)
useSafeScheduledUpdates(safeAddress)
const sendFunds = safeActionsState.sendFunds as { isOpen: boolean; selectedToken: string }
const sendFunds = safeActionsState.sendFunds
const formattedTotalBalance = currentSafeBalance ? formatAmountInUsFormat(currentSafeBalance) : ''
const balance =
!!formattedTotalBalance && !!currentCurrency ? `${formattedTotalBalance} ${currentCurrency}` : undefined
@ -138,10 +144,11 @@ const App: React.FC = ({ children }) => {
<Modal
description="Receive Tokens Form"
handleClose={onReceiveHide}
open={safeActionsState.showReceive as boolean}
open={safeActionsState.showReceive}
paperClassName={classes.receiveModal}
title="Receive Tokens"
>
<Receive onClose={onReceiveHide} safeAddress={safeAddress} safeName={safeName} />
<ReceiveModal onClose={onReceiveHide} safeAddress={safeAddress} safeName={safeName} />
</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 { getNetworkInfo } from 'src/config'
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 = () => ({
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>
</>
)
type Props = {
className: string
}
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 Paragraph from 'src/components/layout/Paragraph'
import { getNetwork } from 'src/config'
import { border, md, screenSm, sm, xs } from 'src/theme/variables'
import { getNetworkInfo } from 'src/config'
import { border, md, screenSm, sm, xs, fontColor } from 'src/theme/variables'
const interfaceNetwork = getNetwork()
const formatNetwork = (network: string): string => network[0].toUpperCase() + network.substring(1).toLowerCase()
const networkInfo = getNetworkInfo()
const useStyles = makeStyles({
container: {
@ -19,7 +18,8 @@ const useStyles = makeStyles({
},
},
text: {
background: border,
backgroundColor: `${networkInfo?.backgroundColor ?? border}`,
color: `${networkInfo?.textColor ?? fontColor}`,
borderRadius: '3px',
lineHeight: 'normal',
margin: '0',
@ -31,18 +31,13 @@ const useStyles = makeStyles({
},
})
interface NetworkLabelProps {
network?: string
}
const NetworkLabel = ({ network = interfaceNetwork }: NetworkLabelProps): React.ReactElement => {
const NetworkLabel = (): React.ReactElement => {
const classes = useStyles()
const formattedNetwork = formatNetwork(network)
return (
<Col className={classes.container} middle="xs" start="xs">
<Paragraph className={classes.text} size="xs">
{formattedNetwork}
{networkInfo.label}
</Paragraph>
</Col>
)

View File

@ -2,11 +2,12 @@ import { withStyles } from '@material-ui/core/styles'
import * as React from 'react'
import ConnectButton from 'src/components/ConnectButton'
import CircleDot from 'src/components/AppLayout/Header/components/CircleDot'
import Block from 'src/components/layout/Block'
import Paragraph from 'src/components/layout/Paragraph'
import Row from 'src/components/layout/Row'
import { lg, md } from 'src/theme/variables'
import { KeyRing } from 'src/components/AppLayout/Header/components/KeyRing'
const styles = () => ({
container: {
@ -42,7 +43,7 @@ const ConnectDetails = ({ classes }) => (
</Row>
</div>
<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>
<Block className={classes.connect}>
<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 classNames from 'classnames'
import * as React from 'react'
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 Block from 'src/components/layout/Block'
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 { background, connected as connectedBg, lg, md, sm, warning, xs } from 'src/theme/variables'
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 styles = () => ({
const styles = createStyles({
container: {
padding: `${md} 12px`,
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 color = connected ? 'primary' : 'warning'
const explorerUrl = getExplorerInfo(userAddress)
const classes = useStyles()
return (
<>
@ -99,12 +122,12 @@ const UserDetails = ({ classes, connected, network, onDisconnect, openDashboard,
{connected ? (
<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>
<Block className={classes.user} justify="center">
{userAddress ? (
<EthHashInfo hash={userAddress} showCopyBtn showEtherscanBtn shortenHash={4} network={network} />
<EthHashInfo hash={userAddress} showCopyBtn explorerUrl={explorerUrl} shortenHash={4} />
) : (
'Address not available'
)}
@ -138,9 +161,9 @@ const UserDetails = ({ classes, connected, network, onDisconnect, openDashboard,
Network
</Paragraph>
<Spacer />
<Img alt="Network" className={classes.logo} height={14} src={dot} />
<CircleDot className={classes.logo} />
<Paragraph align="right" className={classes.labels} noMargin weight="bolder">
{upperFirst(network)}
{upperFirst(ETHEREUM_NETWORK[network])}
</Paragraph>
</Row>
<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 NetworkLabel from '../NetworkLabel'
import CircleDot from 'src/components/AppLayout/Header/components/CircleDot'
import Col from 'src/components/layout/Col'
import Paragraph from 'src/components/layout/Paragraph'
import WalletIcon from '../WalletIcon'
import { connected as connectedBg, screenSm, sm } from 'src/theme/variables'
import { KeyRing } from 'src/components/AppLayout/Header/components/KeyRing'
const useStyles = makeStyles({
network: {
@ -62,16 +62,16 @@ const useStyles = makeStyles({
interface ProviderInfoProps {
connected: boolean
provider: string
network: string
// TODO: [xDai] Review. This may cause some issues with EthHashInfo.
userAddress: string
}
const ProviderInfo = ({ connected, provider, userAddress, network }: ProviderInfoProps): React.ReactElement => {
const ProviderInfo = ({ connected, provider, userAddress }: ProviderInfoProps): React.ReactElement => {
const classes = useStyles()
const addressColor = connected ? 'text' : 'warning'
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()} />
<Col className={classes.account} layout="column" start="sm">
<Paragraph
@ -93,7 +93,6 @@ const ProviderInfo = ({ connected, provider, userAddress, network }: ProviderInf
identiconSize="xs"
textColor={addressColor}
textSize="sm"
network={network}
/>
) : (
<Text size="md" color={addressColor}>
@ -107,7 +106,7 @@ const ProviderInfo = ({ connected, provider, userAddress, network }: ProviderInf
</div>
</Col>
<Col className={classes.networkLabel} layout="column" start="sm">
<NetworkLabel network={network} />
<NetworkLabel />
</Col>
</>
)

View File

@ -1,11 +1,10 @@
import { withStyles } from '@material-ui/core/styles'
import * as React from 'react'
import CircleDot from 'src/components/AppLayout/Header/components/CircleDot'
import Col from 'src/components/layout/Col'
import Paragraph from 'src/components/layout/Paragraph'
import { sm } from 'src/theme/variables'
import { KeyRing } from 'src/components/AppLayout/Header/components/KeyRing'
const styles = () => ({
network: {
@ -27,7 +26,7 @@ const styles = () => ({
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">
<Paragraph
className={classes.network}

View File

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

View File

@ -8,11 +8,13 @@ import {
Identicon,
Button,
CopyToClipboardBtn,
EtherscanButton,
ExplorerButton,
} from '@gnosis.pm/safe-react-components'
import { getNetwork } from 'src/config'
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'
@ -46,6 +48,19 @@ const StyledButton = styled(Button)`
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)`
p {
color: ${({ theme }) => theme.colors.placeHolder};
@ -110,8 +125,14 @@ const SafeHeader = ({
</Container>
)
}
const explorerUrl = getExplorerInfo(address)
const networkInfo = getNetworkInfo()
return (
<>
<StyledTextLabel size="sm" networkInfo={networkInfo}>
{networkInfo.label}
</StyledTextLabel>
<Container>
<IdenticonContainer>
<FlexSpacer />
@ -128,7 +149,7 @@ const SafeHeader = ({
<Icon size="sm" type="qrCode" tooltip="Show QR" />
</UnStyledButton>
<CopyToClipboardBtn textToCopy={address} />
<EtherscanButton value={address} network={getNetwork()} />
<ExplorerButton explorerUrl={explorerUrl} />
</IconContainer>
{granted ? null : (
@ -147,6 +168,7 @@ const SafeHeader = ({
</Text>
</StyledButton>
</Container>
</>
)
}

View File

@ -4,8 +4,13 @@ import { useRouteMatch } from 'react-router-dom'
import { ListItemType } from 'src/components/List'
import ListIcon from 'src/components/List/ListIcon'
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 featuresEnabled = useSelector(safeFeaturesEnabledSelector)
const safeAppsEnabled = Boolean(featuresEnabled?.includes(FEATURES.SAFE_APPS))
const matchSafe = useRouteMatch({ path: `${SAFELIST_ADDRESS}`, strict: false })
const matchSafeWithAddress = useRouteMatch<{ safeAddress: string }>({ path: `${SAFELIST_ADDRESS}/:safeAddress` })
const matchSafeWithAction = useRouteMatch({ path: `${SAFELIST_ADDRESS}/:safeAddress/:safeAction` }) as {
@ -13,11 +18,30 @@ const useSidebarItems = (): ListItemType[] => {
params: Record<string, string>
}
const sidebarItems = useMemo((): ListItemType[] => {
return useMemo((): ListItemType[] => {
if (!matchSafe || !matchSafeWithAddress) {
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 [
{
label: 'ASSETS',
@ -37,22 +61,9 @@ const useSidebarItems = (): ListItemType[] => {
selected: matchSafeWithAction?.params.safeAction === 'address-book',
href: `${matchSafeWithAddress?.url}/address-book`,
},
{
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`,
},
...safeSidebar,
]
}, [matchSafe, matchSafeWithAction, matchSafeWithAddress])
return sidebarItems
}, [matchSafe, matchSafeWithAction, matchSafeWithAddress, safeAppsEnabled])
}
export { useSidebarItems }

View File

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

View File

@ -8,10 +8,9 @@ import { fetchProvider } from 'src/logic/wallets/store/actions'
import transactionDataCheck from 'src/logic/wallets/transactionDataCheck'
import { getSupportedWallets } from 'src/logic/wallets/utils/walletList'
import { store } from 'src/store'
import { BLOCKNATIVE_KEY } from 'src/utils/constants'
const isMainnet = process.env.REACT_APP_NETWORK === 'mainnet'
const BLOCKNATIVE_API_KEY = isMainnet ? process.env.REACT_APP_BLOCKNATIVE_KEY : '7fbb9cee-7e97-4436-8770-8b29a9a8814c'
const networkId = getNetworkId()
let lastUsedAddress = ''
let providerName
@ -19,8 +18,8 @@ let providerName
const wallets = getSupportedWallets()
export const onboard = Onboard({
dappId: BLOCKNATIVE_API_KEY,
networkId: getNetworkId(),
dappId: BLOCKNATIVE_KEY,
networkId: networkId,
subscriptions: {
wallet: (wallet) => {
if (wallet.provider) {

View File

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

View File

@ -17,11 +17,10 @@ interface EtherscanLinkProps {
className?: string
cut?: number
knownAddress?: boolean
type: 'tx' | 'address'
value: string
}
const EtherscanLink = ({ className, cut, knownAddress, type, value }: EtherscanLinkProps): React.ReactElement => {
const EtherscanLink = ({ className, cut, knownAddress, value }: EtherscanLinkProps): React.ReactElement => {
const classes = useStyles()
return (
@ -30,7 +29,7 @@ const EtherscanLink = ({ className, cut, knownAddress, type, value }: EtherscanL
{cut ? shortVersionOf(value, cut) : value}
</Span>
<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}
</Block>
)

View File

@ -3,7 +3,7 @@ import styled from 'styled-components'
export const Wrapper = styled.div`
display: grid;
grid-template-columns: 245px auto;
min-height: 560px;
min-height: 75vh;
.background {
box-shadow: 1px 2px 10px 0 rgba(212, 212, 211, 0.59);
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 ListItem from '@material-ui/core/ListItem'
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 styled from 'styled-components'
import { SafeRecord } 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 { getNetwork } from 'src/config'
import DefaultBadge from './DefaultBadge'
import Hairline from 'src/components/layout/Hairline'
import Link from 'src/components/layout/Link'
import { formatAmount } from 'src/logic/tokens/utils/formatAmount'
import { sameAddress } from 'src/logic/wallets/ethAddresses'
import { SAFELIST_ADDRESS } from 'src/routes/routes'
import { AddressWrapper } from './AddresWrapper'
export const SIDEBAR_SAFELIST_ROW_TESTID = 'SIDEBAR_SAFELIST_ROW_TESTID'
const StyledIcon = styled(Icon)`
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({
list: {
@ -107,34 +67,7 @@ const SafeList = ({ currentSafe, defaultSafe, onSafeClick, safes, setDefaultSafe
) : (
<div className={classes.noIcon}>placeholder</div>
)}
<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>
<AddressWrapper safe={safe} defaultSafe={defaultSafe} setDefaultSafe={setDefaultSafe} />
</ListItem>
</Link>
<Hairline />

View File

@ -1,11 +1,17 @@
import classNames from 'classnames/bind'
import React from 'react'
import React, { ReactElement, ImgHTMLAttributes } from 'react'
import styles from './index.module.scss'
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)
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 { 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 = () => {
if (process.env.NODE_ENV === 'test') {
return testConfig
export const getNetworkId = (): ETHEREUM_NETWORK => ETHEREUM_NETWORK[NETWORK]
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') {
if (process.env.REACT_APP_NETWORK === 'mainnet') {
return process.env.REACT_APP_ENV === 'production'
? mainnetProdConfig
: mainnetStagingConfig
// lookup the config file based on the network specified in the NETWORK variable
const configFile = networks[getNetworkName().toLowerCase()]
// defaults to 'production' as it's the only environment that is required for the network configs
const networkBaseConfig = configFile.environment[currentEnvironment] ?? configFile.environment.production
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'
? prodConfig
: stagingConfig
return getConfig().rpcServiceUrl
}
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'
? mainnetDevConfig
: devConfig
if (apiKey) {
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}`,
)
const getNetworkExplorerApiKey = (networkExplorerName: string): string | undefined=> {
switch (networkExplorerName.toLowerCase()) {
case 'etherscan': {
return ETHERSCAN_API_KEY
}
default: {
return undefined
}
}
}
export const getNetwork = () =>
process.env.REACT_APP_NETWORK === 'mainnet'
? ETHEREUM_NETWORK.MAINNET
: ETHEREUM_NETWORK.RINKEBY
export const getContractABI = async (contractAddress: string) =>{
const { apiUrl, name } = getNetworkExplorerInfo()
export const getNetworkId = () =>
process.env.REACT_APP_NETWORK === 'mainnet' ? 1 : 4
const apiKey = getNetworkExplorerApiKey(name)
const getConfig = ensureOnce(configuration)
try {
const { result, status } = await fetchContractABI(apiUrl, contractAddress, apiKey)
export const getTxServiceHost = () => {
const config = getConfig()
if (status === '0') {
return []
}
return config[TX_SERVICE_HOST]
return result
} catch (e) {
console.error('Failed to retrieve ABI', e)
return undefined
}
}
export const getTxServiceUriFrom = (safeAddress) =>
`safes/${safeAddress}/transactions/`
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 type BlockScanInfo = () => {
alt: string
url: string
}
export const getGnosisSafeAppsUrl = () => {
const config = getConfig()
export const getExplorerInfo = (hash: string): BlockScanInfo => {
const { name, url } = getNetworkExplorerInfo()
const networkInfo = getNetworkInfo()
return config[SAFE_APPS_URL]
}
export const getGoogleAnalyticsTrackingID = () =>
getNetwork() === ETHEREUM_NETWORK.MAINNET
? process.env.REACT_APP_GOOGLE_ANALYTICS_ID_MAINNET
: 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}`
switch (networkInfo.id) {
default: {
const type = hash.length > 42 ? 'tx' : 'address'
return () => ({
url: `${url}/${type}/${hash}`,
alt: name || '',
})
}
}
}

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 enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar'
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]
@ -17,6 +20,7 @@ const addressBookMiddleware = (store) => (next) => async (action) => {
const state = store.getState()
const { dispatch } = store
const addressBook = addressBookSelector(state)
const safes = safesListSelector(state)
if (addressBook.length) {
await saveAddressBook(addressBook)
}
@ -36,8 +40,13 @@ const addressBookMiddleware = (store) => (next) => async (action) => {
break
}
case UPDATE_ENTRY: {
const { entry } = action.payload
const notification = getNotificationsFromTxType(TX_NOTIFICATION_TYPES.ADDRESSBOOK_EDIT_ENTRY)
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
}
default:

View File

@ -29,9 +29,7 @@ export default handleActions(
const entryFound = state.find((oldEntry) => oldEntry.address === entry.address)
// Only adds entries with valid names
const validName = getValidAddressBookName(entry.name)
if (!entryFound && validName) {
if (!entryFound) {
state.push(entry)
}
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 { 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 { 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 {
_rateLimit = async (): Promise<void> => {}
@ -133,14 +83,11 @@ class OpenSea {
* Fetches from OpenSea the list of collectibles, grouped by category,
* for the provided Safe Address in the specified Network
* @param {string} safeAddress
* @param {string} network
* @returns {Promise<Collectibles>}
*/
async fetchAllUserCollectiblesByCategoryAsync(safeAddress: string, network: string): Promise<Collectibles> {
// eslint-disable-next-line no-underscore-dangle
const metadataSourceUrl = this._endpointsUrls[network]
async fetchCollectibles(safeAddress: string): Promise<Collectibles> {
const metadataSourceUrl = this._endpointsUrls[getNetworkId()]
const url = `${metadataSourceUrl}/assets/?owner=${safeAddress}`
// eslint-disable-next-line no-underscore-dangle
const assetsResponse = await this._fetch(url)
const assetsResponseJson = await assetsResponse.json()
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 OpenSea from 'src/logic/collectibles/sources/OpenSea'
import Gnosis from 'src/logic/collectibles/sources/Gnosis'
import { COLLECTIBLES_SOURCE } from 'src/utils/constants'
const SOURCES = {
opensea: new OpenSea({ rps: 4 }),
gnosis: new Gnosis({ rps: 4 }),
mockedopensea: new MockedOpenSea({ rps: 4 }),
}
type Sources = typeof SOURCES
export const getConfiguredSource = (): Sources['opensea'] | Sources['mockedopensea'] =>
export const getConfiguredSource = (): Sources['opensea'] | Sources['mockedopensea'] | Sources['gnosis'] =>
SOURCES[COLLECTIBLES_SOURCE.toLowerCase()]

View File

@ -1,15 +1,13 @@
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'
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 {
const network = getNetwork()
const source = getConfiguredSource()
const collectibles = await source.fetchAllUserCollectiblesByCategoryAsync(safeAddress, network)
const collectibles = await source.fetchCollectibles(safeAddress)
batch(() => {
dispatch(addNftAssets(collectibles.nftAssets))
@ -19,5 +17,3 @@ const fetchCollectibles = (safeAddress: string) => async (dispatch: Dispatch): P
console.log('Error fetching collectibles:', error)
}
}
export default fetchCollectibles

View File

@ -1,22 +1,33 @@
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 { NFT_ASSETS_REDUCER_ID, NFT_TOKENS_REDUCER_ID } from 'src/logic/collectibles/store/reducer/collectibles'
import { safeActiveAssetsSelector } from 'src/logic/safe/store/selectors'
export const nftAssetsSelector = (state: AppReduxState): NFTAssets => state[NFT_ASSETS_REDUCER_ID]
export const nftTokensSelector = (state: AppReduxState): NFTTokens => state[NFT_TOKENS_REDUCER_ID]
export const nftAssets = (state: AppReduxState): NFTAssets => state[NFT_ASSETS_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) : []
})
export const availableNftAssetsAddresses = createSelector(nftTokensSelector, (userNftTokens): string[] => {
return Array.from(new Set(userNftTokens.map((nftToken) => nftToken.assetAddress)))
})
export const activeNftAssetsListSelector = createSelector(
nftAssetsListSelector,
safeActiveAssetsSelector,
(assets, activeAssetsList): NFTAsset[] => {
return assets.filter(({ address }) => activeAssetsList.has(address))
availableNftAssetsAddresses,
(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 { BatchRequest } from 'web3-core'
import { AbiItem } from 'web3-utils'
/**
* 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
* @returns {Promise<[*]>}
*/
const generateBatchRequests = ({ abi, address, batch, context, methods }: any): any => {
const contractInstance: any = new web3.eth.Contract(abi, address)
type MethodsArgsType = Array<string | number>
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 values = methods.map((methodObject) => {
let method, type, args = []
let method, type, args: MethodsArgsType = []
if (typeof methodObject === 'string') {
method = methodObject
} else {
;({ method, type, args = [] } = methodObject)
({ method, type, args } = methodObject)
}
return new Promise((resolve) => {
const resolver = (error, result) => {
if (error) {
resolve(null)
resolve()
} else {
resolve(result)
}
@ -43,7 +55,8 @@ const generateBatchRequests = ({ abi, address, batch, context, methods }: any):
// If batch was provided add to external batch
batch ? batch.add(request) : localBatch.add(request)
} 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.
!batch && localBatch.execute()
const returnValues = context ? [context, ...values] : values
return Promise.all(returnValues)
// @ts-ignore
return Promise.all([context, ...values])
}
export default generateBatchRequests

View File

@ -1,17 +1,17 @@
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 SafeProxy from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafeProxy.json'
import { ensureOnce } from 'src/utils/singleton'
import memoize from 'lodash.memoize'
import { getWeb3, getNetworkIdFrom } from 'src/logic/wallets/getWeb3'
import { calculateGasOf, calculateGasPrice } from 'src/logic/wallets/ethTransactions'
import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses'
import ProxyFactorySol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafeProxyFactory.json'
import SafeProxy from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafeProxy.json'
import Web3 from 'web3'
import { ETHEREUM_NETWORK } from 'src/config/networks/network.d'
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 { GnosisSafeProxyFactory } from 'src/types/contracts/GnosisSafeProxyFactory.d'
export const SENTINEL_ADDRESS = '0x0000000000000000000000000000000000000001'
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'
let proxyFactoryMaster
let safeMaster
let proxyFactoryMaster: GnosisSafeProxyFactory
let safeMaster: GnosisSafe
const createGnosisSafeContract = (web3: Web3) => {
const gnosisSafe = contract(GnosisSafeSol)
gnosisSafe.setProvider(web3.currentProvider)
return gnosisSafe
/**
* Creates a Contract instance of the GnosisSafe contract
* @param {Web3} web3
* @param {ETHEREUM_NETWORK} networkId
*/
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
const proxyFactory = new web3.eth.Contract(ProxyFactorySol.abi as AbiItem[], contractAddress) as unknown as GnosisSafeProxyFactory
return proxyFactory
/**
* Creates a Contract instance of the GnosisSafeProxyFactory contract
* @param {Web3} web3
* @param {ETHEREUM_NETWORK} networkId
*/
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)
const getCreateProxyFactoryContract = memoize(createProxyFactoryContract)
const instantiateMasterCopies = async () => {
@ -47,25 +62,11 @@ const instantiateMasterCopies = async () => {
// Create ProxyFactory Master Copy
proxyFactoryMaster = getCreateProxyFactoryContract(web3, networkId)
// Initialize Safe master copy
const GnosisSafe = getGnosisSafeContract(web3)
safeMaster = await GnosisSafe.deployed()
// Create Safe Master copy
safeMaster = getGnosisSafeContract(web3, networkId)
}
// ONLY USED IN TEST ENVIRONMENT
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 initContracts = instantiateMasterCopies
export const getSafeMasterContract = async () => {
await initContracts()
@ -74,11 +75,11 @@ export const getSafeMasterContract = async () => {
}
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)
.encodeABI()
return proxyFactoryMaster.methods.createProxy(safeMaster.address, gnosisSafeData)
return proxyFactoryMaster.methods.createProxy(safeMaster.options.address, gnosisSafeData)
}
export const estimateGasForDeployingSafe = async (
@ -86,13 +87,13 @@ export const estimateGasForDeployingSafe = async (
numConfirmations,
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)
.encodeABI()
const proxyFactoryData = proxyFactoryMaster.methods
.createProxy(safeMaster.address, gnosisSafeData)
.createProxy(safeMaster.options.address, gnosisSafeData)
.encodeABI()
const gas = await calculateGasOf(proxyFactoryData, userAccount, proxyFactoryMaster.address)
const gas = await calculateGasOf(proxyFactoryData, userAccount, proxyFactoryMaster.options.address)
const gasPrice = await calculateGasPrice()
return gas * parseInt(gasPrice, 10)

View File

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

View File

@ -1,31 +1,32 @@
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 { 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 (
baseCurrency: AVAILABLE_CURRENCIES,
targetCurrencyValue: AVAILABLE_CURRENCIES,
baseCurrency: string,
targetCurrencyValue: string,
safeAddress: string,
): Promise<number> => {
let rate = 0
if (targetCurrencyValue === AVAILABLE_CURRENCIES.ETH) {
if (sameString(targetCurrencyValue, AVAILABLE_CURRENCIES.NETWORK)) {
try {
const result = await fetchTokenCurrenciesBalances(safeAddress)
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) {
console.error('Fetching ETH data from the relayer errored', error)
console.error(`Fetching ${AVAILABLE_CURRENCIES.NETWORK} data from the relayer errored`, error)
}
return rate
}
// National currencies
try {
const url = `${getExchangeRatesUrl()}?base=${baseCurrency}&symbols=${targetCurrencyValue}`
const url = `${EXCHANGE_RATE_URL}?base=${baseCurrency}&symbols=${targetCurrencyValue}`
const result = await axios.get(url)
if (result?.data) {
const { rates } = result.data

View File

@ -1,28 +1,22 @@
import axios, { AxiosResponse } from 'axios'
import { getTxServiceHost } from 'src/config'
import { getSafeServiceBaseUrl } from 'src/config'
import { TokenProps } from 'src/logic/tokens/store/model/token'
export type BalanceEndpoint = {
balance: string
balanceUsd: string
tokenAddress: string
token?: TokenProps
usdConversion: string
balance: string
fiatBalance: string
fiatConversion: string
fiatCode: string
}
const fetchTokenCurrenciesBalances = (
export const fetchTokenCurrenciesBalances = (
safeAddress: string,
excludeSpamTokens = true,
): Promise<AxiosResponse<BalanceEndpoint[]>> => {
const apiUrl = getTxServiceHost()
const url = `${apiUrl}safes/${safeAddress}/balances/usd/?exclude_spam=${excludeSpamTokens}`
const url = `${getSafeServiceBaseUrl(safeAddress)}/balances/usd/?exclude_spam=${excludeSpamTokens}`
return axios.get(url, {
params: {
limit: 3000,
},
})
return axios.get(url)
}
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 { Dispatch } from 'redux'
const fetchCurrencyRate = (safeAddress: string, selectedCurrency: AVAILABLE_CURRENCIES) => async (
const fetchCurrencyRate = (safeAddress: string, selectedCurrency: string) => async (
dispatch: Dispatch<typeof setCurrencyRate>,
): Promise<void> => {
if (AVAILABLE_CURRENCIES.USD === selectedCurrency) {

View File

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

View File

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

View File

@ -1,8 +1,7 @@
import { loadFromStorage, saveToStorage } from 'src/utils/storage'
import { AVAILABLE_CURRENCIES } from '../model/currencyValues'
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 {
await saveToStorage(SELECTED_CURRENCY_STORAGE_KEY, selectedCurrency)
} 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)
}

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import { useEffect, useRef } from 'react'
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 fetchEtherBalance from 'src/logic/safe/store/actions/fetchEtherBalance'
import { checkAndUpdateSafe } from 'src/logic/safe/store/actions/fetchSafe'

View File

@ -572,18 +572,6 @@ describe('calculateTransactionStatus', () => {
// then
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', () => {
// given
const userAddress = 'address1'
@ -762,7 +750,36 @@ describe('calculateTransactionType', () => {
describe('buildTx', () => {
it('Returns a valid transaction', async () => {
// 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 userAddress = 'address1'
const cancellationTxs = List([cancelTx1])
@ -776,7 +793,7 @@ describe('buildTx', () => {
})
const knownTokens = Map<string, Record<TokenProps> & Readonly<TokenProps>>()
knownTokens.set('0x00Df91984582e6e96288307E9c2f20b38C8FeCE9', token)
const outgoingTxs = List([cancelTx1])
const outgoingTxs = [cancelTx1]
const safeInstance = makeSafe({ name: 'LOADED SAFE', address: safeAddress })
const expectedTx = makeTransaction({
baseGas: 0,
@ -826,7 +843,7 @@ describe('buildTx', () => {
outgoingTxs,
safe: safeInstance,
tx: transaction,
txCode: null,
txCode: undefined,
})
// then

View File

@ -1,9 +1,18 @@
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 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,
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 { getAllTransactionsUriFrom, getTxServiceHost } from 'src/config'
import { getSafeServiceBaseUrl } from 'src/config'
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 = {
safeAddress: string
@ -21,11 +21,8 @@ type TransactionDTO = {
}
const getAllTransactionsUri = (safeAddress: string): string => {
const host = getTxServiceHost()
const address = checksumAddress(safeAddress)
const base = getAllTransactionsUriFrom(address)
return `${host}${base}`
return `${getSafeServiceBaseUrl(address)}/all-transactions/`
}
const fetchAllTransactions = async (

View File

@ -16,7 +16,7 @@ import {
saveTxToHistory,
tryOffchainSigning,
} 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 { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses'
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 { PayableTx } from 'src/types/contracts/types.d'
import { AppReduxState } from 'src/store'
import { Dispatch } from './types'
import { Dispatch, DispatchReturn } from './types'
export const removeTxFromStore = (
tx: Transaction,
@ -107,9 +107,10 @@ interface CreateTransactionArgs {
txData?: string
txNonce?: number | 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 ErrorEventHandler = () => void
@ -124,10 +125,11 @@ const createTransaction = (
operation = CALL,
navigateToTransactionsTab = true,
origin = null,
safeTxGas: safeTxGasArg,
}: CreateTransactionArgs,
onUserConfirm?: ConfirmEventHandler,
onError?: ErrorEventHandler,
): CreateTransactionAction => async (dispatch: Dispatch, getState: () => AppReduxState): Promise<void> => {
): CreateTransactionAction => async (dispatch: Dispatch, getState: () => AppReduxState): Promise<DispatchReturn> => {
const state = getState()
if (navigateToTransactionsTab) {
@ -143,7 +145,8 @@ const createTransaction = (
const nonce = await getNewTxNonce(txNonce?.toString(), lastTx, safeInstance)
const isExecution = await shouldExecuteTransaction(safeInstance, nonce, lastTx)
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
const sigs = `0x000000000000000000000000${from.replace(

View File

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

View File

@ -1,7 +1,7 @@
import axios from 'axios'
import { List } from 'immutable'
import { buildSafeCreationTxUrl } from 'src/config'
import { buildSafeCreationTxUrl } from 'src/logic/safe/utils/buildSafeCreationTxUrl'
import { addOrUpdateTransactions } from './transactions/addOrUpdateTransactions'
import { makeTransaction } from 'src/logic/safe/store/models/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 { loadFromStorage } from 'src/utils/storage'
import { addSafe } from './addSafe'
import { addOrUpdateSafe } from './addOrUpdateSafe'
const loadSafesFromStorage = () => async (dispatch: Dispatch): Promise<void> => {
try {
@ -13,7 +13,7 @@ const loadSafesFromStorage = () => async (dispatch: Dispatch): Promise<void> =>
if (safes) {
Object.values(safes).forEach((safeProps) => {
dispatch(addSafe(buildSafe(safeProps), true))
dispatch(addOrUpdateSafe(buildSafe(safeProps), true))
})
}
} catch (err) {

View File

@ -13,6 +13,8 @@ const getServiceUrl = (txType: string, safeAddress: string): string => {
}[txType](safeAddress)
}
// TODO: Remove this magic
/* eslint-disable */
async function fetchTransactions(
txType: TransactionTypes.INCOMING,
safeAddress: string,
@ -28,6 +30,7 @@ async function fetchTransactions(
safeAddress: string,
eTag: string | null,
): Promise<{ eTag: string | null; results: TxServiceModel[] | IncomingTxServiceModel[] }> {
/* eslint-enable */
try {
const url = getServiceUrl(txType, safeAddress)
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 { 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 { ALTERNATIVE_TOKEN_ABI } from 'src/logic/tokens/utils/alternativeAbi'
import { web3ReadOnly } from 'src/logic/wallets/getWeb3'
@ -43,6 +46,7 @@ const buildIncomingTransactionFrom = ([tx, symbol, decimals, fee]: [
const batchIncomingTxsTokenDataRequest = (txs: IncomingTxServiceModel[]) => {
const batch = new web3ReadOnly.BatchRequest()
const { nativeCoin } = getNetworkInfo()
const whenTxsValues = txs.map((tx) => {
const methods = [
@ -52,8 +56,16 @@ const batchIncomingTxsTokenDataRequest = (txs: IncomingTxServiceModel[]) => {
{ method: 'getTransactionReceipt', args: [tx.transactionHash], type: 'eth' },
]
return generateBatchRequests({
abi: ALTERNATIVE_TOKEN_ABI,
return generateBatchRequests<
[
IncomingTxServiceModel,
string | undefined,
string | undefined,
Transaction | undefined,
TransactionReceipt | undefined,
]
>({
abi: ALTERNATIVE_TOKEN_ABI as AbiItem[],
address: tx.tokenAddress,
batch,
context: tx,
@ -64,11 +76,11 @@ const batchIncomingTxsTokenDataRequest = (txs: IncomingTxServiceModel[]) => {
batch.execute()
return Promise.all(whenTxsValues).then((txsValues) =>
txsValues.map(([tx, symbol, decimals, { gasPrice }, { gasUsed }]) => [
txsValues.map(([tx, symbol, decimals, ethTx, ethTxReceipt]) => [
tx,
symbol === null ? 'ETH' : symbol,
decimals === null ? '18' : decimals,
new bn(gasPrice).times(gasUsed),
symbol ? symbol : nativeCoin.symbol,
decimals ? decimals : nativeCoin.decimals,
new bn(ethTx?.gasPrice ?? 0).times(ethTxReceipt?.gasUsed ?? 0),
]),
)
}

View File

@ -1,7 +1,7 @@
import { fromJS, List, Map } from 'immutable'
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 { PROVIDER_REDUCER_ID } from 'src/logic/wallets/store/reducer/provider'
import { buildTx, isCancelTransaction } from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers'
@ -59,8 +59,8 @@ export type SafeTransactionsType = {
}
export type OutgoingTxs = {
cancellationTxs: any
outgoingTxs: any
cancellationTxs: Record<number, TxServiceModel>
outgoingTxs: TxServiceModel[]
}
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
* @param transactions
* @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)) {
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 whenTxsValues = transactions.map((tx) => {
return generateBatchRequests({
return generateBatchRequests<BatchRequestReturnValues>({
abi: [],
address: tx.to,
batch,
@ -141,7 +143,7 @@ const batchProcessOutgoingTransactions = async ({
safe,
}: BatchProcessTxsProps): Promise<{
cancel: Record<string, Transaction>
outgoing: Array<Transaction>
outgoing: Transaction[]
}> => {
// cancellation transactions
const cancelTxsValues = Object.values(cancellationTxs)
@ -193,9 +195,9 @@ export const loadOutgoingTransactions = async (safeAddress: string): Promise<Saf
return defaultResponse
}
const knownTokens = state[TOKEN_REDUCER_ID]
const currentUser = state[PROVIDER_REDUCER_ID].get('account')
const safe = state[SAFE_REDUCER_ID].getIn(['safes', safeAddress])
const knownTokens: TokenState = state[TOKEN_REDUCER_ID]
const currentUser: string = state[PROVIDER_REDUCER_ID].get('account')
const safe: SafeRecord = state[SAFE_REDUCER_ID].getIn(['safes', safeAddress])
if (!safe) {
return defaultResponse

View File

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

View File

@ -3,4 +3,6 @@ import { AnyAction } from 'redux'
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 { grantedSelector } from 'src/routes/safe/container/selector'
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 updateSafe from 'src/logic/safe/store/actions/updateSafe'
import {
@ -20,8 +19,9 @@ import {
} from 'src/logic/safe/store/selectors'
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 (
dispatch,
@ -146,7 +146,7 @@ const notificationsMiddleware = (store) => (next) => async (action) => {
})
break
}
case ADD_SAFE: {
case ADD_OR_UPDATE_SAFE: {
const state = store.getState()
const { safe } = action.payload
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 { tokensSelector } from 'src/logic/tokens/store/selectors'
import { saveActiveTokens } from 'src/logic/tokens/utils/tokensStorage'
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 { EDIT_SAFE_OWNER } from 'src/logic/safe/store/actions/editSafeOwner'
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_ASSETS_LIST } from 'src/logic/safe/store/actions/updateAssetsList'
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 { 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 = [
ADD_SAFE,
UPDATE_SAFE,
REMOVE_SAFE,
ADD_OR_UPDATE_SAFE,
@ -60,7 +54,6 @@ const safeStorageMware = (store) => (next) => async (action) => {
const state = store.getState()
const { dispatch } = store
const safes = safesMapSelector(state)
const addressBook = addressBookSelector(state)
await saveSafes(safes.toJSON())
switch (action.type) {
@ -68,43 +61,6 @@ const safeStorageMware = (store) => (next) => async (action) => {
recalculateActiveTokens(state)
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: {
const { safe } = action.payload
safe.owners.forEach((owner) => {

View File

@ -1,4 +1,5 @@
import { List, Map, Record, RecordOf, Set } from 'immutable'
import { FEATURES } from 'src/config/networks/network.d'
export type SafeOwner = {
name: string
@ -13,7 +14,7 @@ export type SafeRecordProps = {
threshold: number
ethBalance: string
owners: List<SafeOwner>
modules: ModulePair[] | null
modules?: ModulePair[] | null
activeTokens: Set<string>
activeAssets: Set<string>
blacklistedTokens: Set<string>
@ -24,7 +25,7 @@ export type SafeRecordProps = {
recurringUser?: boolean
currentVersion: string
needsUpdate: boolean
featuresEnabled: Array<string>
featuresEnabled: Array<FEATURES>
}
const makeSafe = Record<SafeRecordProps>({
@ -38,7 +39,7 @@ const makeSafe = Record<SafeRecordProps>({
activeAssets: Set(),
blacklistedTokens: Set(),
blacklistedAssets: Set(),
balances: Map({}),
balances: Map(),
nonce: 0,
latestIncomingTxBlock: 0,
recurringUser: undefined,

View File

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

View File

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

View File

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

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