commit
319ad7554e
|
@ -1,13 +1,8 @@
|
||||||
# You can leave this empty for rinkeby or use "mainnet"
|
# You can leave this empty for rinkeby or use "mainnet"
|
||||||
REACT_APP_NETWORK=
|
REACT_APP_NETWORK=
|
||||||
|
|
||||||
# For Rinkeby network
|
|
||||||
REACT_APP_GOOGLE_ANALYTICS_ID_RINKEBY=
|
|
||||||
|
|
||||||
# For Mainnet network (no needed on dev mode)
|
|
||||||
REACT_APP_GOOGLE_ANALYTICS_ID_MAINNET=
|
|
||||||
|
|
||||||
# For all environments
|
# For all environments
|
||||||
|
REACT_APP_GOOGLE_ANALYTICS=
|
||||||
REACT_APP_INFURA_TOKEN=
|
REACT_APP_INFURA_TOKEN=
|
||||||
REACT_APP_IPFS_GATEWAY=https://ipfs.io/ipfs
|
REACT_APP_IPFS_GATEWAY=https://ipfs.io/ipfs
|
||||||
PUBLIC_URL=/app/
|
PUBLIC_URL=/app/
|
||||||
|
|
32
.travis.yml
32
.travis.yml
|
@ -1,4 +1,4 @@
|
||||||
if: (branch = development) OR (branch = master) OR (type = pull_request) OR (tag IS present)
|
if: (branch = development) OR (branch = master) OR (release/v2.13.0) OR (type = pull_request) OR (tag IS present)
|
||||||
sudo: required
|
sudo: required
|
||||||
dist: bionic
|
dist: bionic
|
||||||
language: node_js
|
language: node_js
|
||||||
|
@ -10,12 +10,19 @@ matrix:
|
||||||
include:
|
include:
|
||||||
- env:
|
- env:
|
||||||
- REACT_APP_NETWORK='mainnet'
|
- REACT_APP_NETWORK='mainnet'
|
||||||
|
- REACT_APP_GOOGLE_ANALYTICS=${REACT_APP_GOOGLE_ANALYTICS_ID_MAINNET}
|
||||||
- STAGING_BUCKET_NAME=${STAGING_MAINNET_BUCKET_NAME}
|
- STAGING_BUCKET_NAME=${STAGING_MAINNET_BUCKET_NAME}
|
||||||
- REACT_APP_GNOSIS_APPS_URL=${REACT_APP_GNOSIS_APPS_URL_PROD}
|
- REACT_APP_GNOSIS_APPS_URL=${REACT_APP_GNOSIS_APPS_URL_PROD}
|
||||||
if: (branch = master AND NOT type = pull_request) OR tag IS present
|
if: (branch = master AND NOT type = pull_request) OR tag IS present
|
||||||
- env:
|
- env:
|
||||||
- REACT_APP_NETWORK='rinkeby'
|
- REACT_APP_NETWORK='rinkeby'
|
||||||
|
- REACT_APP_GOOGLE_ANALYTICS=${REACT_APP_GOOGLE_ANALYTICS_ID_RINKEBY}
|
||||||
- REACT_APP_GNOSIS_APPS_URL=${REACT_APP_GNOSIS_APPS_URL_STAGING}
|
- REACT_APP_GNOSIS_APPS_URL=${REACT_APP_GNOSIS_APPS_URL_STAGING}
|
||||||
|
- env:
|
||||||
|
- REACT_APP_NETWORK='xdai'
|
||||||
|
- REACT_APP_GOOGLE_ANALYTICS=${REACT_APP_GOOGLE_ANALYTICS_ID_XDAI}
|
||||||
|
- STAGING_BUCKET_NAME=${STAGING_XDAI_BUCKET_NAME}
|
||||||
|
if: ((branch = master OR branch = release/v2.13.0) AND NOT type = pull_request) OR tag IS present
|
||||||
cache:
|
cache:
|
||||||
yarn: true
|
yarn: true
|
||||||
before_script:
|
before_script:
|
||||||
|
@ -47,31 +54,44 @@ deploy:
|
||||||
secret_access_key: $AWS_SECRET_ACCESS_KEY
|
secret_access_key: $AWS_SECRET_ACCESS_KEY
|
||||||
skip_cleanup: true
|
skip_cleanup: true
|
||||||
local_dir: build
|
local_dir: build
|
||||||
upload-dir: app
|
upload_dir: app
|
||||||
region: $AWS_DEFAULT_REGION
|
region: $AWS_DEFAULT_REGION
|
||||||
on:
|
on:
|
||||||
branch: development
|
branch: development
|
||||||
|
|
||||||
# Staging environment
|
# Staging environment
|
||||||
- provider: s3
|
- provider: s3
|
||||||
bucket: $STAGING_BUCKET_NAME
|
bucket: $STAGING_BUCKET_NAME
|
||||||
access_key_id: $AWS_ACCESS_KEY_ID
|
access_key_id: $AWS_ACCESS_KEY_ID
|
||||||
secret_access_key: $AWS_SECRET_ACCESS_KEY
|
secret_access_key: $AWS_SECRET_ACCESS_KEY
|
||||||
skip_cleanup: true
|
skip_cleanup: true
|
||||||
local_dir: build
|
local_dir: build
|
||||||
upload-dir: current/app
|
upload_dir: current/app
|
||||||
region: $AWS_DEFAULT_REGION
|
region: $AWS_DEFAULT_REGION
|
||||||
on:
|
on:
|
||||||
branch: master
|
branch: master
|
||||||
|
|
||||||
# Prepare production deployment
|
# xDai testing on staging
|
||||||
|
- provider: s3
|
||||||
|
bucket: $STAGING_BUCKET_NAME
|
||||||
|
access_key_id: $AWS_ACCESS_KEY_ID
|
||||||
|
secret_access_key: $AWS_SECRET_ACCESS_KEY
|
||||||
|
skip_cleanup: true
|
||||||
|
local_dir: build
|
||||||
|
upload_dir: current/app
|
||||||
|
region: $AWS_DEFAULT_REGION
|
||||||
|
on:
|
||||||
|
branch: release/v2.13.0
|
||||||
|
condition: $REACT_APP_NETWORK = xdai
|
||||||
|
|
||||||
|
# Prepare production deployment
|
||||||
- provider: s3
|
- provider: s3
|
||||||
bucket: $STAGING_BUCKET_NAME
|
bucket: $STAGING_BUCKET_NAME
|
||||||
secret_access_key: $AWS_SECRET_ACCESS_KEY
|
secret_access_key: $AWS_SECRET_ACCESS_KEY
|
||||||
access_key_id: $AWS_ACCESS_KEY_ID
|
access_key_id: $AWS_ACCESS_KEY_ID
|
||||||
skip_cleanup: true
|
skip_cleanup: true
|
||||||
local_dir: build
|
local_dir: build
|
||||||
upload-dir: releases/$TRAVIS_TAG
|
upload_dir: releases/$TRAVIS_TAG
|
||||||
region: $AWS_DEFAULT_REGION
|
region: $AWS_DEFAULT_REGION
|
||||||
on:
|
on:
|
||||||
tags: true
|
tags: true
|
||||||
|
|
|
@ -5,7 +5,7 @@ module.exports = function override(config) {
|
||||||
config.plugins = []
|
config.plugins = []
|
||||||
}
|
}
|
||||||
config.plugins.push(
|
config.plugins.push(
|
||||||
new webpack.ContextReplacementPlugin(/truffle-(contract|interface-adapter)/, (data) => {
|
new webpack.ContextReplacementPlugin(/@truffle\/(contract|interface-adapter)/, (data) => {
|
||||||
delete data.dependencies[0].critical
|
delete data.dependencies[0].critical
|
||||||
return data
|
return data
|
||||||
}),
|
}),
|
||||||
|
|
47
package.json
47
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "safe-react",
|
"name": "safe-react",
|
||||||
"version": "2.12.3",
|
"version": "2.13.0",
|
||||||
"description": "Allowing crypto users manage funds in a safer way",
|
"description": "Allowing crypto users manage funds in a safer way",
|
||||||
"website": "https://github.com/gnosis/safe-react#readme",
|
"website": "https://github.com/gnosis/safe-react#readme",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
|
@ -61,8 +61,7 @@
|
||||||
"src/**/*.{js,jsx,ts,tsx}",
|
"src/**/*.{js,jsx,ts,tsx}",
|
||||||
"!src/**/*.{.test.*}",
|
"!src/**/*.{.test.*}",
|
||||||
"!src/**/test/**/*",
|
"!src/**/test/**/*",
|
||||||
"!src/**/assets/**",
|
"!src/**/assets/**"
|
||||||
"!src/config/**/*"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"productName": "Safe Multisig",
|
"productName": "Safe Multisig",
|
||||||
|
@ -164,36 +163,38 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@gnosis.pm/safe-apps-sdk": "0.4.0",
|
"@gnosis.pm/safe-apps-sdk": "0.4.2",
|
||||||
"@gnosis.pm/safe-contracts": "1.1.1-dev.2",
|
"@gnosis.pm/safe-contracts": "1.1.1-dev.2",
|
||||||
"@gnosis.pm/safe-react-components": "https://github.com/gnosis/safe-react-components.git#1bf397f",
|
"@gnosis.pm/safe-react-components": "https://github.com/gnosis/safe-react-components.git#70e57bdd1e0fd5dfdf5768076577c1e000b5fe28",
|
||||||
"@gnosis.pm/util-contracts": "2.0.6",
|
"@gnosis.pm/util-contracts": "2.0.6",
|
||||||
"@ledgerhq/hw-transport-node-hid": "5.22.0",
|
"@ledgerhq/hw-transport-node-hid": "5.26.0",
|
||||||
"@material-ui/core": "4.11.0",
|
"@material-ui/core": "4.11.0",
|
||||||
"@material-ui/icons": "4.9.1",
|
"@material-ui/icons": "4.9.1",
|
||||||
"@material-ui/lab": "4.0.0-alpha.56",
|
"@material-ui/lab": "4.0.0-alpha.56",
|
||||||
"@openzeppelin/contracts": "3.1.0",
|
"@openzeppelin/contracts": "3.1.0",
|
||||||
|
"@truffle/contract": "4.2.25",
|
||||||
"async-sema": "^3.1.0",
|
"async-sema": "^3.1.0",
|
||||||
"axios": "0.20.0",
|
"axios": "0.20.0",
|
||||||
"bignumber.js": "9.0.0",
|
"bignumber.js": "9.0.1",
|
||||||
"bnc-onboard": "1.13.1",
|
"bnc-onboard": "1.13.2",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
"concurrently": "^5.3.0",
|
"concurrently": "^5.3.0",
|
||||||
"connected-react-router": "6.8.0",
|
"connected-react-router": "6.8.0",
|
||||||
"coveralls": "^3.1.0",
|
"coveralls": "^3.1.0",
|
||||||
"currency-flags": "2.1.2",
|
"currency-flags": "2.1.2",
|
||||||
"date-fns": "2.15.0",
|
"date-fns": "2.16.1",
|
||||||
|
"detect-port": "^1.3.0",
|
||||||
"electron-is-dev": "^1.2.0",
|
"electron-is-dev": "^1.2.0",
|
||||||
"electron-log": "4.2.4",
|
"electron-log": "4.2.4",
|
||||||
"electron-settings": "^4.0.2",
|
"electron-settings": "^4.0.2",
|
||||||
"electron-updater": "4.3.4",
|
"electron-updater": "4.3.5",
|
||||||
"eth-sig-util": "^2.5.3",
|
"eth-sig-util": "^2.5.3",
|
||||||
"ethereum-blockies-base64": "^1.0.2",
|
"ethereum-blockies-base64": "^1.0.2",
|
||||||
"ethereumjs-abi": "0.6.8",
|
"ethereumjs-abi": "0.6.8",
|
||||||
"exponential-backoff": "^3.1.0",
|
"exponential-backoff": "^3.1.0",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"final-form": "^4.20.1",
|
"final-form": "^4.20.1",
|
||||||
"final-form-calculate": "^1.3.1",
|
"final-form-calculate": "^1.3.2",
|
||||||
"history": "4.10.1",
|
"history": "4.10.1",
|
||||||
"immortal-db": "^1.1.0",
|
"immortal-db": "^1.1.0",
|
||||||
"immutable": "^4.0.0-rc.12",
|
"immutable": "^4.0.0-rc.12",
|
||||||
|
@ -202,16 +203,15 @@
|
||||||
"lodash.memoize": "^4.1.2",
|
"lodash.memoize": "^4.1.2",
|
||||||
"material-ui-search-bar": "^1.0.0",
|
"material-ui-search-bar": "^1.0.0",
|
||||||
"notistack": "https://github.com/gnosis/notistack.git#v0.9.4",
|
"notistack": "https://github.com/gnosis/notistack.git#v0.9.4",
|
||||||
"open": "^7.2.0",
|
|
||||||
"polished": "3.6.7",
|
"polished": "3.6.7",
|
||||||
"qrcode.react": "1.0.0",
|
"qrcode.react": "1.0.0",
|
||||||
"query-string": "6.13.1",
|
"query-string": "6.13.5",
|
||||||
"react": "16.13.1",
|
"react": "16.13.1",
|
||||||
"react-dom": "16.13.1",
|
"react-dom": "16.13.1",
|
||||||
"react-final-form": "^6.5.1",
|
"react-final-form": "^6.5.1",
|
||||||
"react-final-form-listeners": "^1.0.2",
|
"react-final-form-listeners": "^1.0.2",
|
||||||
"react-ga": "3.1.2",
|
"react-ga": "3.1.2",
|
||||||
"react-hot-loader": "4.12.21",
|
"react-hot-loader": "4.13.0",
|
||||||
"react-qr-reader": "^2.2.1",
|
"react-qr-reader": "^2.2.1",
|
||||||
"react-redux": "7.2.1",
|
"react-redux": "7.2.1",
|
||||||
"react-router-dom": "5.2.0",
|
"react-router-dom": "5.2.0",
|
||||||
|
@ -224,7 +224,6 @@
|
||||||
"reselect": "^4.0.0",
|
"reselect": "^4.0.0",
|
||||||
"semver": "7.3.2",
|
"semver": "7.3.2",
|
||||||
"styled-components": "^5.2.0",
|
"styled-components": "^5.2.0",
|
||||||
"truffle-contract": "4.0.31",
|
|
||||||
"web3": "1.2.9",
|
"web3": "1.2.9",
|
||||||
"web3-core": "^1.2.11",
|
"web3-core": "^1.2.11",
|
||||||
"web3-eth-contract": "^1.2.11",
|
"web3-eth-contract": "^1.2.11",
|
||||||
|
@ -242,12 +241,12 @@
|
||||||
"@types/history": "4.6.2",
|
"@types/history": "4.6.2",
|
||||||
"@types/jest": "^26.0.14",
|
"@types/jest": "^26.0.14",
|
||||||
"@types/lodash.memoize": "^4.1.6",
|
"@types/lodash.memoize": "^4.1.6",
|
||||||
"@types/node": "14.11.2",
|
"@types/node": "^14.11.8",
|
||||||
"@types/react": "^16.9.49",
|
"@types/react": "^16.9.52",
|
||||||
"@types/react-dom": "^16.9.6",
|
"@types/react-dom": "^16.9.6",
|
||||||
"@types/react-redux": "^7.1.9",
|
"@types/react-redux": "^7.1.9",
|
||||||
"@types/react-router-dom": "^5.1.5",
|
"@types/react-router-dom": "^5.1.6",
|
||||||
"@types/styled-components": "^5.1.3",
|
"@types/styled-components": "^5.1.4",
|
||||||
"@typescript-eslint/eslint-plugin": "3.9.1",
|
"@typescript-eslint/eslint-plugin": "3.9.1",
|
||||||
"@typescript-eslint/parser": "3.9.1",
|
"@typescript-eslint/parser": "3.9.1",
|
||||||
"autoprefixer": "9.8.6",
|
"autoprefixer": "9.8.6",
|
||||||
|
@ -258,15 +257,15 @@
|
||||||
"electron-builder": "22.8.1",
|
"electron-builder": "22.8.1",
|
||||||
"electron-notarize": "1.0.0",
|
"electron-notarize": "1.0.0",
|
||||||
"eslint": "6.8.0",
|
"eslint": "6.8.0",
|
||||||
"eslint-config-prettier": "6.11.0",
|
"eslint-config-prettier": "6.12.0",
|
||||||
"eslint-plugin-import": "2.22.0",
|
"eslint-plugin-import": "2.22.1",
|
||||||
"eslint-plugin-jsx-a11y": "^6.3.1",
|
"eslint-plugin-jsx-a11y": "^6.3.1",
|
||||||
"eslint-plugin-prettier": "^3.1.4",
|
"eslint-plugin-prettier": "^3.1.4",
|
||||||
"eslint-plugin-react": "^7.20.6",
|
"eslint-plugin-react": "^7.21.4",
|
||||||
"eslint-plugin-sort-destructure-keys": "1.3.5",
|
"eslint-plugin-sort-destructure-keys": "1.3.5",
|
||||||
"ethereumjs-abi": "0.6.8",
|
"ethereumjs-abi": "0.6.8",
|
||||||
"husky": "^4.2.5",
|
"husky": "^4.3.0",
|
||||||
"lint-staged": "10.4.0",
|
"lint-staged": "^10.4.0",
|
||||||
"node-sass": "^4.14.1",
|
"node-sass": "^4.14.1",
|
||||||
"prettier": "2.1.2",
|
"prettier": "2.1.2",
|
||||||
"react-app-rewired": "^2.1.6",
|
"react-app-rewired": "^2.1.6",
|
||||||
|
|
|
@ -1,84 +1,88 @@
|
||||||
const electron = require("electron");
|
const electron = require('electron')
|
||||||
const express = require('express');
|
const express = require('express')
|
||||||
const open = require('open');
|
const log = require('electron-log')
|
||||||
const log = require('electron-log');
|
const fs = require('fs')
|
||||||
const fs = require('fs');
|
const Menu = electron.Menu
|
||||||
const Menu = electron.Menu;
|
const https = require('https')
|
||||||
const https = require('https');
|
const detect = require('detect-port')
|
||||||
const autoUpdater = require('./auto-updater');
|
const autoUpdater = require('./auto-updater')
|
||||||
|
|
||||||
const app = electron.app;
|
const { app, session, BrowserWindow, shell } = electron
|
||||||
const session = electron.session;
|
|
||||||
const BrowserWindow = electron.BrowserWindow;
|
|
||||||
|
|
||||||
const path = require("path");
|
const path = require('path')
|
||||||
const isDev = require("electron-is-dev");
|
const isDev = require('electron-is-dev')
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
key: fs.readFileSync(path.join(__dirname, './ssl/server.key')),
|
key: fs.readFileSync(path.join(__dirname, './ssl/server.key')),
|
||||||
cert: fs.readFileSync(path.join(__dirname, './ssl/server.crt')),
|
cert: fs.readFileSync(path.join(__dirname, './ssl/server.crt')),
|
||||||
ca: fs.readFileSync(path.join(__dirname, './ssl/rootCA.crt'))
|
ca: fs.readFileSync(path.join(__dirname, './ssl/rootCA.crt')),
|
||||||
};
|
|
||||||
|
|
||||||
const PORT = 5000;
|
|
||||||
|
|
||||||
const createServer = () => {
|
|
||||||
const app = express();
|
|
||||||
const staticRoute = path.join(__dirname, '../build');
|
|
||||||
app.use(express.static(staticRoute));
|
|
||||||
https.createServer(options, app).listen(PORT);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DEFAULT_PORT = 5000
|
||||||
|
|
||||||
let mainWindow;
|
const createServer = async () => {
|
||||||
|
const app = express()
|
||||||
|
const staticRoute = path.join(__dirname, '../build')
|
||||||
|
app.use(express.static(staticRoute))
|
||||||
|
let selectedPort = DEFAULT_PORT
|
||||||
|
try {
|
||||||
|
const _port = await detect(DEFAULT_PORT)
|
||||||
|
if (_port !== DEFAULT_PORT) selectedPort = _port
|
||||||
|
https.createServer(options, app).listen(selectedPort)
|
||||||
|
} catch (e) {
|
||||||
|
log.error(e)
|
||||||
|
} finally {
|
||||||
|
return selectedPort
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getOpenedWindow(url,options) {
|
let mainWindow
|
||||||
let display = electron.screen.getPrimaryDisplay();
|
|
||||||
let width = display.bounds.width;
|
function getOpenedWindow(url, options) {
|
||||||
let height = display.bounds.height;
|
let display = electron.screen.getPrimaryDisplay()
|
||||||
|
let width = display.bounds.width
|
||||||
|
let height = display.bounds.height
|
||||||
|
|
||||||
// filter all requests to trezor-bridge and change origin to make it work
|
// filter all requests to trezor-bridge and change origin to make it work
|
||||||
const filter = {
|
const filter = {
|
||||||
urls: ['http://127.0.0.1:21325/*']
|
urls: ['http://127.0.0.1:21325/*'],
|
||||||
};
|
|
||||||
|
|
||||||
options.webPreferences.affinity = 'main-window';
|
|
||||||
|
|
||||||
if(url.includes('trezor')){
|
|
||||||
session.defaultSession.webRequest.onBeforeSendHeaders(filter, (details, callback) => {
|
|
||||||
details.requestHeaders['Origin'] = 'https://connect.trezor.io';
|
|
||||||
callback({cancel: false, requestHeaders: details.requestHeaders});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if(url.includes('wallet.portis') || url.includes('trezor') || url.includes('app.tor.us')){
|
options.webPreferences.affinity = 'main-window'
|
||||||
|
|
||||||
|
if (url.includes('trezor')) {
|
||||||
|
session.defaultSession.webRequest.onBeforeSendHeaders(filter, (details, callback) => {
|
||||||
|
details.requestHeaders['Origin'] = 'https://connect.trezor.io'
|
||||||
|
callback({ cancel: false, requestHeaders: details.requestHeaders })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.includes('wallet.portis') || url.includes('trezor') || url.includes('app.tor.us')) {
|
||||||
const win = new BrowserWindow({
|
const win = new BrowserWindow({
|
||||||
width:350,
|
width: 350,
|
||||||
height:700,
|
height: 700,
|
||||||
x: width - 1300,
|
x: width - 1300,
|
||||||
parent:mainWindow,
|
parent: mainWindow,
|
||||||
y: height - (process.platform === 'win32' ? 750 : 200),
|
y: height - (process.platform === 'win32' ? 750 : 200),
|
||||||
webContents: options.webContents, // use existing webContents if provided
|
webContents: options.webContents, // use existing webContents if provided
|
||||||
fullscreen: false,
|
fullscreen: false,
|
||||||
show: false,
|
show: false,
|
||||||
});
|
})
|
||||||
win.webContents.on('new-window', function(event, url){
|
win.webContents.on('new-window', function (event, url) {
|
||||||
if(url.includes('trezor') && url.includes('bridge'))
|
if (url.includes('trezor') && url.includes('bridge')) shell.openExternal(url)
|
||||||
open(url);
|
})
|
||||||
});
|
win.once('ready-to-show', () => win.show())
|
||||||
win.once('ready-to-show', () => win.show());
|
|
||||||
|
|
||||||
if(!options.webPreferences){
|
if (!options.webPreferences) {
|
||||||
win.loadURL(url);
|
win.loadURL(url)
|
||||||
}
|
}
|
||||||
return win
|
return win
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function createWindow() {
|
function createWindow(port = DEFAULT_PORT) {
|
||||||
mainWindow = new BrowserWindow({
|
mainWindow = new BrowserWindow({
|
||||||
show: false,
|
show: false,
|
||||||
width: 1024,
|
width: 1024,
|
||||||
|
@ -89,79 +93,77 @@ function createWindow() {
|
||||||
nativeWindowOpen: true, // need to be set in order to display modal
|
nativeWindowOpen: true, // need to be set in order to display modal
|
||||||
},
|
},
|
||||||
icon: electron.nativeImage.createFromPath(path.join(__dirname, './build/safe.png')),
|
icon: electron.nativeImage.createFromPath(path.join(__dirname, './build/safe.png')),
|
||||||
});
|
})
|
||||||
|
|
||||||
mainWindow.once('ready-to-show', () => {
|
mainWindow.once('ready-to-show', () => {
|
||||||
mainWindow.show();
|
mainWindow.show()
|
||||||
});
|
})
|
||||||
|
|
||||||
mainWindow.loadURL(
|
mainWindow.loadURL(isDev ? 'http://localhost:3000' : `https://localhost:${port}`)
|
||||||
isDev
|
|
||||||
? "http://localhost:3000"
|
|
||||||
: `https://localhost:${PORT}`
|
|
||||||
)
|
|
||||||
|
|
||||||
if (isDev) {
|
if (isDev) {
|
||||||
// Open the DevTools.
|
// Open the DevTools.
|
||||||
mainWindow.webContents.openDevTools();
|
mainWindow.webContents.openDevTools()
|
||||||
//BrowserWindow.addDevToolsExtension('<location to your react chrome extension>');
|
//BrowserWindow.addDevToolsExtension('<location to your react chrome extension>');
|
||||||
}
|
}
|
||||||
|
|
||||||
mainWindow.setMenu(null);
|
mainWindow.setMenu(null)
|
||||||
mainWindow.setMenuBarVisibility(false);
|
mainWindow.setMenuBarVisibility(false)
|
||||||
|
|
||||||
mainWindow.webContents.on('new-window', function(event, url, frameName, disposition, options){
|
mainWindow.webContents.on('new-window', function (event, url, frameName, disposition, options) {
|
||||||
event.preventDefault();
|
event.preventDefault()
|
||||||
const win = getOpenedWindow(url,options);
|
const win = getOpenedWindow(url, options)
|
||||||
if(win){
|
if (win) {
|
||||||
win.once('ready-to-show', () => win.show());
|
win.once('ready-to-show', () => win.show())
|
||||||
|
|
||||||
if(!options.webPreferences){
|
if (!options.webPreferences) {
|
||||||
win.loadURL(url);
|
win.loadURL(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
event.newGuest = win
|
event.newGuest = win
|
||||||
} else open(url);
|
} else shell.openExternal(url)
|
||||||
});
|
})
|
||||||
|
|
||||||
mainWindow.webContents.on('did-finish-load', () => {
|
mainWindow.webContents.on('did-finish-load', () => {
|
||||||
autoUpdater.init(mainWindow);
|
autoUpdater.init(mainWindow)
|
||||||
});
|
})
|
||||||
|
|
||||||
mainWindow.webContents.on('crashed', (event) => {
|
mainWindow.webContents.on('crashed', (event) => {
|
||||||
log.info(`App Crashed: ${event}`);
|
log.info(`App Crashed: ${event}`)
|
||||||
mainWindow.reload();
|
mainWindow.reload()
|
||||||
});
|
})
|
||||||
|
|
||||||
mainWindow.on("closed", () => (mainWindow = null));
|
mainWindow.on('closed', () => (mainWindow = null))
|
||||||
}
|
}
|
||||||
|
|
||||||
process.on('uncaughtException',function(error){
|
process.on('uncaughtException', function (error) {
|
||||||
log.error(error);
|
log.error(error)
|
||||||
});
|
})
|
||||||
|
|
||||||
app.userAgentFallback = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) old-airport-include/1.0.0 Chrome Electron/7.1.7 Safari/537.36';
|
app.userAgentFallback =
|
||||||
|
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) old-airport-include/1.0.0 Chrome Electron/7.1.7 Safari/537.36'
|
||||||
|
|
||||||
// We have one non-context-aware module in node_modules/usb. This is used by @ledgerhq/hw-transport-node-hid
|
// We have one non-context-aware module in node_modules/usb. This is used by @ledgerhq/hw-transport-node-hid
|
||||||
// This type of modules will be impossible to use after electron 10
|
// This type of modules will be impossible to use after electron 10
|
||||||
app.allowRendererProcessReuse = false;
|
app.allowRendererProcessReuse = false
|
||||||
|
|
||||||
app.commandLine.appendSwitch('ignore-certificate-errors');
|
app.commandLine.appendSwitch('ignore-certificate-errors')
|
||||||
app.on("ready", () =>{
|
app.on('ready', async () => {
|
||||||
// Hide the menu
|
// Hide the menu
|
||||||
Menu.setApplicationMenu(null);
|
Menu.setApplicationMenu(null)
|
||||||
if(!isDev) createServer();
|
let usedPort = DEFAULT_PORT
|
||||||
createWindow();
|
if (!isDev) usedPort = await createServer()
|
||||||
});
|
createWindow(usedPort)
|
||||||
|
})
|
||||||
|
|
||||||
app.on("window-all-closed", () => {
|
app.on('window-all-closed', () => {
|
||||||
if (process.platform !== "darwin") {
|
if (process.platform !== 'darwin') {
|
||||||
app.quit();
|
app.quit()
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
app.on("activate", () => {
|
app.on('activate', () => {
|
||||||
if (mainWindow === null) {
|
if (mainWindow === null) {
|
||||||
createWindow();
|
createWindow()
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import styled from 'styled-components'
|
import { getNetworkInfo } from 'src/config'
|
||||||
|
|
||||||
import CopyBtn from 'src/components/CopyBtn'
|
import CopyBtn from 'src/components/CopyBtn'
|
||||||
import EtherscanBtn from 'src/components/EtherscanBtn'
|
import EtherscanBtn from 'src/components/EtherscanBtn'
|
||||||
import Identicon from 'src/components/Identicon'
|
import Identicon from 'src/components/Identicon'
|
||||||
|
@ -8,6 +7,7 @@ import Block from 'src/components/layout/Block'
|
||||||
import Bold from 'src/components/layout/Bold'
|
import Bold from 'src/components/layout/Bold'
|
||||||
import Paragraph from 'src/components/layout/Paragraph'
|
import Paragraph from 'src/components/layout/Paragraph'
|
||||||
import { border, xs } from 'src/theme/variables'
|
import { border, xs } from 'src/theme/variables'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
const Wrapper = styled.div`
|
const Wrapper = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -42,6 +42,8 @@ interface Props {
|
||||||
ethBalance?: string
|
ethBalance?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { nativeCoin } = getNetworkInfo()
|
||||||
|
|
||||||
const AddressInfo = ({ ethBalance, safeAddress, safeName }: Props): React.ReactElement => {
|
const AddressInfo = ({ ethBalance, safeAddress, safeName }: Props): React.ReactElement => {
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
|
@ -59,12 +61,12 @@ const AddressInfo = ({ ethBalance, safeAddress, safeName }: Props): React.ReactE
|
||||||
{safeAddress}
|
{safeAddress}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<CopyBtn content={safeAddress} />
|
<CopyBtn content={safeAddress} />
|
||||||
<EtherscanBtn type="address" value={safeAddress} />
|
<EtherscanBtn value={safeAddress} />
|
||||||
</div>
|
</div>
|
||||||
{ethBalance && (
|
{ethBalance && (
|
||||||
<StyledBlock>
|
<StyledBlock>
|
||||||
<Paragraph noMargin>
|
<Paragraph noMargin>
|
||||||
Balance: <Bold data-testid="current-eth-balance">{`${ethBalance} ETH`}</Bold>
|
Balance: <Bold data-testid="current-eth-balance">{`${ethBalance} ${nativeCoin.symbol}`}</Bold>
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
</StyledBlock>
|
</StyledBlock>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import IconButton from '@material-ui/core/IconButton'
|
||||||
import { createStyles, makeStyles } from '@material-ui/core/styles'
|
import { createStyles, makeStyles } from '@material-ui/core/styles'
|
||||||
import Close from '@material-ui/icons/Close'
|
import Close from '@material-ui/icons/Close'
|
||||||
import QRCode from 'qrcode.react'
|
import QRCode from 'qrcode.react'
|
||||||
import * as React from 'react'
|
import React, { ReactElement } from 'react'
|
||||||
|
|
||||||
import CopyBtn from 'src/components/CopyBtn'
|
import CopyBtn from 'src/components/CopyBtn'
|
||||||
import EtherscanBtn from 'src/components/EtherscanBtn'
|
import EtherscanBtn from 'src/components/EtherscanBtn'
|
||||||
|
@ -13,9 +13,11 @@ import Col from 'src/components/layout/Col'
|
||||||
import Hairline from 'src/components/layout/Hairline'
|
import Hairline from 'src/components/layout/Hairline'
|
||||||
import Paragraph from 'src/components/layout/Paragraph'
|
import Paragraph from 'src/components/layout/Paragraph'
|
||||||
import Row from 'src/components/layout/Row'
|
import Row from 'src/components/layout/Row'
|
||||||
import { lg, md, screenSm, secondaryText, sm } from 'src/theme/variables'
|
import { border, fontColor, lg, md, screenSm, secondaryText, sm } from 'src/theme/variables'
|
||||||
import { copyToClipboard } from 'src/utils/clipboard'
|
import { copyToClipboard } from 'src/utils/clipboard'
|
||||||
|
import { getNetworkInfo } from 'src/config'
|
||||||
|
|
||||||
|
const networkInfo = getNetworkInfo()
|
||||||
const useStyles = makeStyles(
|
const useStyles = makeStyles(
|
||||||
createStyles({
|
createStyles({
|
||||||
heading: {
|
heading: {
|
||||||
|
@ -35,6 +37,12 @@ const useStyles = makeStyles(
|
||||||
borderRadius: '6px',
|
borderRadius: '6px',
|
||||||
border: `1px solid ${secondaryText}`,
|
border: `1px solid ${secondaryText}`,
|
||||||
},
|
},
|
||||||
|
networkInfo: {
|
||||||
|
backgroundColor: `${networkInfo?.backgroundColor ?? border}`,
|
||||||
|
color: `${networkInfo?.textColor ?? fontColor}`,
|
||||||
|
padding: md,
|
||||||
|
marginBottom: 0,
|
||||||
|
},
|
||||||
annotation: {
|
annotation: {
|
||||||
margin: lg,
|
margin: lg,
|
||||||
marginBottom: 0,
|
marginBottom: 0,
|
||||||
|
@ -79,7 +87,7 @@ type Props = {
|
||||||
safeName: string
|
safeName: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const ReceiveModal = ({ onClose, safeAddress, safeName }: Props) => {
|
const ReceiveModal = ({ onClose, safeAddress, safeName }: Props): ReactElement => {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -93,9 +101,12 @@ const ReceiveModal = ({ onClose, safeAddress, safeName }: Props) => {
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Row>
|
</Row>
|
||||||
<Hairline />
|
<Hairline />
|
||||||
|
<Paragraph className={classes.networkInfo} noMargin size="lg" weight="bolder">
|
||||||
|
{networkInfo.label} Network only send {networkInfo.label} assets to this Safe.
|
||||||
|
</Paragraph>
|
||||||
<Paragraph className={classes.annotation} noMargin size="lg">
|
<Paragraph className={classes.annotation} noMargin size="lg">
|
||||||
This is the address of your Safe. Deposit funds by scanning the QR code or copying the address below. Only send
|
This is the address of your Safe. Deposit funds by scanning the QR code or copying the address below. Only send{' '}
|
||||||
ETH and ERC-20 tokens to this address!
|
{networkInfo.nativeCoin.name} and ERC-20 tokens to this address!
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<Col layout="column" middle="xs">
|
<Col layout="column" middle="xs">
|
||||||
<Paragraph className={classes.safeName} noMargin size="lg" weight="bold">
|
<Paragraph className={classes.safeName} noMargin size="lg" weight="bold">
|
||||||
|
@ -115,7 +126,7 @@ const ReceiveModal = ({ onClose, safeAddress, safeName }: Props) => {
|
||||||
{safeAddress}
|
{safeAddress}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<CopyBtn content={safeAddress} />
|
<CopyBtn content={safeAddress} />
|
||||||
<EtherscanBtn type="address" value={safeAddress} />
|
<EtherscanBtn value={safeAddress} />
|
||||||
</Block>
|
</Block>
|
||||||
</Col>
|
</Col>
|
||||||
<Hairline />
|
<Hairline />
|
||||||
|
|
|
@ -16,8 +16,8 @@ import CookiesBanner from 'src/components/CookiesBanner'
|
||||||
import Notifier from 'src/components/Notifier'
|
import Notifier from 'src/components/Notifier'
|
||||||
import Backdrop from 'src/components/layout/Backdrop'
|
import Backdrop from 'src/components/layout/Backdrop'
|
||||||
import Img from 'src/components/layout/Img'
|
import Img from 'src/components/layout/Img'
|
||||||
import { getNetwork } from 'src/config'
|
import { getNetworkId } from 'src/config'
|
||||||
import { ETHEREUM_NETWORK } from 'src/logic/wallets/getWeb3'
|
import { ETHEREUM_NETWORK } from 'src/config/networks/network.d'
|
||||||
import { networkSelector } from 'src/logic/wallets/store/selectors'
|
import { networkSelector } from 'src/logic/wallets/store/selectors'
|
||||||
import { SAFELIST_ADDRESS, WELCOME_ADDRESS } from 'src/routes/routes'
|
import { SAFELIST_ADDRESS, WELCOME_ADDRESS } from 'src/routes/routes'
|
||||||
import { safeNameSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
import { safeNameSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
||||||
|
@ -26,11 +26,11 @@ import SendModal from 'src/routes/safe/components/Balances/SendModal'
|
||||||
import { useLoadSafe } from 'src/logic/safe/hooks/useLoadSafe'
|
import { useLoadSafe } from 'src/logic/safe/hooks/useLoadSafe'
|
||||||
import { useSafeScheduledUpdates } from 'src/logic/safe/hooks/useSafeScheduledUpdates'
|
import { useSafeScheduledUpdates } from 'src/logic/safe/hooks/useSafeScheduledUpdates'
|
||||||
import useSafeActions from 'src/logic/safe/hooks/useSafeActions'
|
import useSafeActions from 'src/logic/safe/hooks/useSafeActions'
|
||||||
import { currentCurrencySelector, safeFiatBalancesTotalSelector } from 'src/logic/currencyValues/store/selectors/index'
|
import { currentCurrencySelector, safeFiatBalancesTotalSelector } from 'src/logic/currencyValues/store/selectors'
|
||||||
import { formatAmountInUsFormat } from 'src/logic/tokens/utils/formatAmount'
|
import { formatAmountInUsFormat } from 'src/logic/tokens/utils/formatAmount'
|
||||||
import { grantedSelector } from 'src/routes/safe/container/selector'
|
import { grantedSelector } from 'src/routes/safe/container/selector'
|
||||||
|
|
||||||
import Receive from './ReceiveModal'
|
import ReceiveModal from './ReceiveModal'
|
||||||
import { useSidebarItems } from 'src/components/AppLayout/Sidebar/useSidebarItems'
|
import { useSidebarItems } from 'src/components/AppLayout/Sidebar/useSidebarItems'
|
||||||
|
|
||||||
const notificationStyles = {
|
const notificationStyles = {
|
||||||
|
@ -46,6 +46,12 @@ const notificationStyles = {
|
||||||
info: {
|
info: {
|
||||||
background: '#fff',
|
background: '#fff',
|
||||||
},
|
},
|
||||||
|
receiveModal: {
|
||||||
|
height: 'auto',
|
||||||
|
maxWidth: 'calc(100% - 30px)',
|
||||||
|
minHeight: '544px',
|
||||||
|
overflow: 'hidden',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const Frame = styled.div`
|
const Frame = styled.div`
|
||||||
|
@ -55,7 +61,7 @@ const Frame = styled.div`
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
`
|
`
|
||||||
|
|
||||||
const desiredNetwork = getNetwork()
|
const desiredNetwork = getNetworkId()
|
||||||
|
|
||||||
const useStyles = makeStyles(notificationStyles)
|
const useStyles = makeStyles(notificationStyles)
|
||||||
|
|
||||||
|
@ -67,7 +73,7 @@ const App: React.FC = ({ children }) => {
|
||||||
const matchSafe = useRouteMatch({ path: `${SAFELIST_ADDRESS}`, strict: false })
|
const matchSafe = useRouteMatch({ path: `${SAFELIST_ADDRESS}`, strict: false })
|
||||||
const history = useHistory()
|
const history = useHistory()
|
||||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||||
const safeName = useSelector(safeNameSelector)
|
const safeName = useSelector(safeNameSelector) ?? ''
|
||||||
const { safeActionsState, onShow, onHide, showSendFunds, hideSendFunds } = useSafeActions()
|
const { safeActionsState, onShow, onHide, showSendFunds, hideSendFunds } = useSafeActions()
|
||||||
const currentSafeBalance = useSelector(safeFiatBalancesTotalSelector)
|
const currentSafeBalance = useSelector(safeFiatBalancesTotalSelector)
|
||||||
const currentCurrency = useSelector(currentCurrencySelector)
|
const currentCurrency = useSelector(currentCurrencySelector)
|
||||||
|
@ -77,7 +83,7 @@ const App: React.FC = ({ children }) => {
|
||||||
useLoadSafe(safeAddress)
|
useLoadSafe(safeAddress)
|
||||||
useSafeScheduledUpdates(safeAddress)
|
useSafeScheduledUpdates(safeAddress)
|
||||||
|
|
||||||
const sendFunds = safeActionsState.sendFunds as { isOpen: boolean; selectedToken: string }
|
const sendFunds = safeActionsState.sendFunds
|
||||||
const formattedTotalBalance = currentSafeBalance ? formatAmountInUsFormat(currentSafeBalance) : ''
|
const formattedTotalBalance = currentSafeBalance ? formatAmountInUsFormat(currentSafeBalance) : ''
|
||||||
const balance =
|
const balance =
|
||||||
!!formattedTotalBalance && !!currentCurrency ? `${formattedTotalBalance} ${currentCurrency}` : undefined
|
!!formattedTotalBalance && !!currentCurrency ? `${formattedTotalBalance} ${currentCurrency}` : undefined
|
||||||
|
@ -138,10 +144,11 @@ const App: React.FC = ({ children }) => {
|
||||||
<Modal
|
<Modal
|
||||||
description="Receive Tokens Form"
|
description="Receive Tokens Form"
|
||||||
handleClose={onReceiveHide}
|
handleClose={onReceiveHide}
|
||||||
open={safeActionsState.showReceive as boolean}
|
open={safeActionsState.showReceive}
|
||||||
|
paperClassName={classes.receiveModal}
|
||||||
title="Receive Tokens"
|
title="Receive Tokens"
|
||||||
>
|
>
|
||||||
<Receive onClose={onReceiveHide} safeAddress={safeAddress} safeName={safeName} />
|
<ReceiveModal onClose={onReceiveHide} safeAddress={safeAddress} safeName={safeName} />
|
||||||
</Modal>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -1,86 +1,27 @@
|
||||||
import { withStyles } from '@material-ui/core/styles'
|
|
||||||
import Dot from '@material-ui/icons/FiberManualRecord'
|
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
import { getNetworkInfo } from 'src/config'
|
||||||
|
|
||||||
import Block from 'src/components/layout/Block'
|
type Props = {
|
||||||
import Img from 'src/components/layout/Img'
|
className: string
|
||||||
import { border, fancy, screenSm, warning } from 'src/theme/variables'
|
|
||||||
|
|
||||||
const key = require('../assets/key.svg')
|
|
||||||
const triangle = require('../assets/triangle.svg')
|
|
||||||
|
|
||||||
const styles = () => ({
|
|
||||||
root: {
|
|
||||||
display: 'none',
|
|
||||||
[`@media (min-width: ${screenSm}px)`]: {
|
|
||||||
display: 'flex',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dot: {
|
|
||||||
position: 'relative',
|
|
||||||
backgroundColor: '#ffffff',
|
|
||||||
color: fancy,
|
|
||||||
},
|
|
||||||
key: {
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
backgroundColor: border,
|
|
||||||
},
|
|
||||||
warning: {
|
|
||||||
position: 'relative',
|
|
||||||
top: '-2px',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const buildKeyStyleFrom = (size, center, dotSize) => ({
|
|
||||||
width: `${size}px`,
|
|
||||||
height: `${size}px`,
|
|
||||||
marginLeft: center ? `${dotSize}px` : 'none',
|
|
||||||
borderRadius: `${size}px`,
|
|
||||||
})
|
|
||||||
|
|
||||||
const buildDotStyleFrom = (size, top, right, mode) => ({
|
|
||||||
width: `${size}px`,
|
|
||||||
height: `${size}px`,
|
|
||||||
borderRadius: `${size}px`,
|
|
||||||
top: `${top}px`,
|
|
||||||
right: `${right}px`,
|
|
||||||
color: mode === 'error' ? fancy : warning,
|
|
||||||
})
|
|
||||||
|
|
||||||
const KeyRing = ({
|
|
||||||
center = false,
|
|
||||||
circleSize,
|
|
||||||
classes,
|
|
||||||
dotRight,
|
|
||||||
dotSize,
|
|
||||||
dotTop,
|
|
||||||
hideDot = false,
|
|
||||||
keySize,
|
|
||||||
mode,
|
|
||||||
}) => {
|
|
||||||
const keyStyle = buildKeyStyleFrom(circleSize, center, dotSize)
|
|
||||||
const dotStyle = buildDotStyleFrom(dotSize, dotTop, dotRight, mode)
|
|
||||||
const isWarning = mode === 'warning'
|
|
||||||
const img = isWarning ? triangle : key
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Block className={classes.root}>
|
|
||||||
<Block className={classes.key} style={keyStyle}>
|
|
||||||
<Img
|
|
||||||
alt="Status connection"
|
|
||||||
className={isWarning ? classes.warning : undefined}
|
|
||||||
height={keySize}
|
|
||||||
src={img}
|
|
||||||
width={isWarning ? keySize + 2 : keySize}
|
|
||||||
/>
|
|
||||||
</Block>
|
|
||||||
{!hideDot && <Dot className={classes.dot} style={dotStyle} />}
|
|
||||||
</Block>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withStyles(styles as any)(KeyRing)
|
export const CircleDot = (props: Props): React.ReactElement => {
|
||||||
|
const networkInfo = getNetworkInfo()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={props.className}>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 10 10">
|
||||||
|
<circle
|
||||||
|
cx="208"
|
||||||
|
cy="203"
|
||||||
|
r="3"
|
||||||
|
fill="none"
|
||||||
|
fillRule="evenodd"
|
||||||
|
stroke={networkInfo?.backgroundColor ?? '#FF685E'}
|
||||||
|
strokeWidth="3"
|
||||||
|
transform="translate(-203 -198)"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -3,11 +3,10 @@ import * as React from 'react'
|
||||||
|
|
||||||
import Col from 'src/components/layout/Col'
|
import Col from 'src/components/layout/Col'
|
||||||
import Paragraph from 'src/components/layout/Paragraph'
|
import Paragraph from 'src/components/layout/Paragraph'
|
||||||
import { getNetwork } from 'src/config'
|
import { getNetworkInfo } from 'src/config'
|
||||||
import { border, md, screenSm, sm, xs } from 'src/theme/variables'
|
import { border, md, screenSm, sm, xs, fontColor } from 'src/theme/variables'
|
||||||
|
|
||||||
const interfaceNetwork = getNetwork()
|
const networkInfo = getNetworkInfo()
|
||||||
const formatNetwork = (network: string): string => network[0].toUpperCase() + network.substring(1).toLowerCase()
|
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
const useStyles = makeStyles({
|
||||||
container: {
|
container: {
|
||||||
|
@ -19,7 +18,8 @@ const useStyles = makeStyles({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
text: {
|
text: {
|
||||||
background: border,
|
backgroundColor: `${networkInfo?.backgroundColor ?? border}`,
|
||||||
|
color: `${networkInfo?.textColor ?? fontColor}`,
|
||||||
borderRadius: '3px',
|
borderRadius: '3px',
|
||||||
lineHeight: 'normal',
|
lineHeight: 'normal',
|
||||||
margin: '0',
|
margin: '0',
|
||||||
|
@ -31,18 +31,13 @@ const useStyles = makeStyles({
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
interface NetworkLabelProps {
|
const NetworkLabel = (): React.ReactElement => {
|
||||||
network?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const NetworkLabel = ({ network = interfaceNetwork }: NetworkLabelProps): React.ReactElement => {
|
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
const formattedNetwork = formatNetwork(network)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col className={classes.container} middle="xs" start="xs">
|
<Col className={classes.container} middle="xs" start="xs">
|
||||||
<Paragraph className={classes.text} size="xs">
|
<Paragraph className={classes.text} size="xs">
|
||||||
{formattedNetwork}
|
{networkInfo.label}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
</Col>
|
</Col>
|
||||||
)
|
)
|
||||||
|
|
|
@ -2,11 +2,12 @@ import { withStyles } from '@material-ui/core/styles'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
import ConnectButton from 'src/components/ConnectButton'
|
import ConnectButton from 'src/components/ConnectButton'
|
||||||
import CircleDot from 'src/components/AppLayout/Header/components/CircleDot'
|
|
||||||
import Block from 'src/components/layout/Block'
|
import Block from 'src/components/layout/Block'
|
||||||
import Paragraph from 'src/components/layout/Paragraph'
|
import Paragraph from 'src/components/layout/Paragraph'
|
||||||
import Row from 'src/components/layout/Row'
|
import Row from 'src/components/layout/Row'
|
||||||
import { lg, md } from 'src/theme/variables'
|
import { lg, md } from 'src/theme/variables'
|
||||||
|
import { KeyRing } from 'src/components/AppLayout/Header/components/KeyRing'
|
||||||
|
|
||||||
const styles = () => ({
|
const styles = () => ({
|
||||||
container: {
|
container: {
|
||||||
|
@ -42,7 +43,7 @@ const ConnectDetails = ({ classes }) => (
|
||||||
</Row>
|
</Row>
|
||||||
</div>
|
</div>
|
||||||
<Row className={classes.logo} margin="lg">
|
<Row className={classes.logo} margin="lg">
|
||||||
<CircleDot center circleSize={75} dotRight={25} dotSize={25} dotTop={50} keySize={32} mode="error" />
|
<KeyRing center circleSize={75} dotRight={25} dotSize={25} dotTop={50} keySize={32} mode="error" />
|
||||||
</Row>
|
</Row>
|
||||||
<Block className={classes.connect}>
|
<Block className={classes.connect}>
|
||||||
<ConnectButton data-testid="heading-connect-btn" />
|
<ConnectButton data-testid="heading-connect-btn" />
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import { withStyles } from '@material-ui/core/styles'
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
import Dot from '@material-ui/icons/FiberManualRecord'
|
import Dot from '@material-ui/icons/FiberManualRecord'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { EthHashInfo, Identicon } from '@gnosis.pm/safe-react-components'
|
import { EthHashInfo, Identicon } from '@gnosis.pm/safe-react-components'
|
||||||
|
|
||||||
import CircleDot from 'src/components/AppLayout/Header/components/CircleDot'
|
|
||||||
import Spacer from 'src/components/Spacer'
|
import Spacer from 'src/components/Spacer'
|
||||||
import Block from 'src/components/layout/Block'
|
import Block from 'src/components/layout/Block'
|
||||||
import Button from 'src/components/layout/Button'
|
import Button from 'src/components/layout/Button'
|
||||||
|
@ -14,11 +13,15 @@ import Paragraph from 'src/components/layout/Paragraph'
|
||||||
import Row from 'src/components/layout/Row'
|
import Row from 'src/components/layout/Row'
|
||||||
import { background, connected as connectedBg, lg, md, sm, warning, xs } from 'src/theme/variables'
|
import { background, connected as connectedBg, lg, md, sm, warning, xs } from 'src/theme/variables'
|
||||||
import { upperFirst } from 'src/utils/css'
|
import { upperFirst } from 'src/utils/css'
|
||||||
|
import { ETHEREUM_NETWORK } from 'src/config/networks/network.d'
|
||||||
|
import { getExplorerInfo } from 'src/config'
|
||||||
|
import { KeyRing } from 'src/components/AppLayout/Header/components/KeyRing'
|
||||||
|
import { CircleDot } from '../CircleDot'
|
||||||
|
import { createStyles } from '@material-ui/core'
|
||||||
|
|
||||||
const dot = require('../../assets/dotRinkeby.svg')
|
|
||||||
const walletIcon = require('../../assets/wallet.svg')
|
const walletIcon = require('../../assets/wallet.svg')
|
||||||
|
|
||||||
const styles = () => ({
|
const styles = createStyles({
|
||||||
container: {
|
container: {
|
||||||
padding: `${md} 12px`,
|
padding: `${md} 12px`,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
@ -88,9 +91,29 @@ const styles = () => ({
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const UserDetails = ({ classes, connected, network, onDisconnect, openDashboard, provider, userAddress }) => {
|
type Props = {
|
||||||
|
connected: boolean
|
||||||
|
network: ETHEREUM_NETWORK
|
||||||
|
onDisconnect: () => void
|
||||||
|
openDashboard?: (() => void | null) | boolean
|
||||||
|
provider?: string
|
||||||
|
userAddress: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
|
export const UserDetails = ({
|
||||||
|
connected,
|
||||||
|
network,
|
||||||
|
onDisconnect,
|
||||||
|
openDashboard,
|
||||||
|
provider,
|
||||||
|
userAddress,
|
||||||
|
}: Props): React.ReactElement => {
|
||||||
const status = connected ? 'Connected' : 'Connection error'
|
const status = connected ? 'Connected' : 'Connection error'
|
||||||
const color = connected ? 'primary' : 'warning'
|
const color = connected ? 'primary' : 'warning'
|
||||||
|
const explorerUrl = getExplorerInfo(userAddress)
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -99,12 +122,12 @@ const UserDetails = ({ classes, connected, network, onDisconnect, openDashboard,
|
||||||
{connected ? (
|
{connected ? (
|
||||||
<Identicon address={userAddress || 'random'} size="lg" />
|
<Identicon address={userAddress || 'random'} size="lg" />
|
||||||
) : (
|
) : (
|
||||||
<CircleDot circleSize={75} dotRight={25} dotSize={25} dotTop={50} hideDot keySize={30} mode="warning" />
|
<KeyRing circleSize={75} dotRight={25} dotSize={25} dotTop={50} hideDot keySize={30} mode="warning" />
|
||||||
)}
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
<Block className={classes.user} justify="center">
|
<Block className={classes.user} justify="center">
|
||||||
{userAddress ? (
|
{userAddress ? (
|
||||||
<EthHashInfo hash={userAddress} showCopyBtn showEtherscanBtn shortenHash={4} network={network} />
|
<EthHashInfo hash={userAddress} showCopyBtn explorerUrl={explorerUrl} shortenHash={4} />
|
||||||
) : (
|
) : (
|
||||||
'Address not available'
|
'Address not available'
|
||||||
)}
|
)}
|
||||||
|
@ -138,9 +161,9 @@ const UserDetails = ({ classes, connected, network, onDisconnect, openDashboard,
|
||||||
Network
|
Network
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<Spacer />
|
<Spacer />
|
||||||
<Img alt="Network" className={classes.logo} height={14} src={dot} />
|
<CircleDot className={classes.logo} />
|
||||||
<Paragraph align="right" className={classes.labels} noMargin weight="bolder">
|
<Paragraph align="right" className={classes.labels} noMargin weight="bolder">
|
||||||
{upperFirst(network)}
|
{upperFirst(ETHEREUM_NETWORK[network])}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
</Row>
|
</Row>
|
||||||
<Hairline margin="xs" />
|
<Hairline margin="xs" />
|
||||||
|
@ -170,5 +193,3 @@ const UserDetails = ({ classes, connected, network, onDisconnect, openDashboard,
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withStyles(styles as any)(UserDetails)
|
|
||||||
|
|
|
@ -3,11 +3,11 @@ import * as React from 'react'
|
||||||
import { EthHashInfo, Text } from '@gnosis.pm/safe-react-components'
|
import { EthHashInfo, Text } from '@gnosis.pm/safe-react-components'
|
||||||
|
|
||||||
import NetworkLabel from '../NetworkLabel'
|
import NetworkLabel from '../NetworkLabel'
|
||||||
import CircleDot from 'src/components/AppLayout/Header/components/CircleDot'
|
|
||||||
import Col from 'src/components/layout/Col'
|
import Col from 'src/components/layout/Col'
|
||||||
import Paragraph from 'src/components/layout/Paragraph'
|
import Paragraph from 'src/components/layout/Paragraph'
|
||||||
import WalletIcon from '../WalletIcon'
|
import WalletIcon from '../WalletIcon'
|
||||||
import { connected as connectedBg, screenSm, sm } from 'src/theme/variables'
|
import { connected as connectedBg, screenSm, sm } from 'src/theme/variables'
|
||||||
|
import { KeyRing } from 'src/components/AppLayout/Header/components/KeyRing'
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
const useStyles = makeStyles({
|
||||||
network: {
|
network: {
|
||||||
|
@ -62,16 +62,16 @@ const useStyles = makeStyles({
|
||||||
interface ProviderInfoProps {
|
interface ProviderInfoProps {
|
||||||
connected: boolean
|
connected: boolean
|
||||||
provider: string
|
provider: string
|
||||||
network: string
|
// TODO: [xDai] Review. This may cause some issues with EthHashInfo.
|
||||||
userAddress: string
|
userAddress: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProviderInfo = ({ connected, provider, userAddress, network }: ProviderInfoProps): React.ReactElement => {
|
const ProviderInfo = ({ connected, provider, userAddress }: ProviderInfoProps): React.ReactElement => {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
const addressColor = connected ? 'text' : 'warning'
|
const addressColor = connected ? 'text' : 'warning'
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!connected && <CircleDot circleSize={35} dotRight={11} dotSize={16} dotTop={24} keySize={14} mode="warning" />}
|
{!connected && <KeyRing circleSize={35} dotRight={11} dotSize={16} dotTop={24} keySize={14} mode="warning" />}
|
||||||
<WalletIcon provider={provider.toUpperCase()} />
|
<WalletIcon provider={provider.toUpperCase()} />
|
||||||
<Col className={classes.account} layout="column" start="sm">
|
<Col className={classes.account} layout="column" start="sm">
|
||||||
<Paragraph
|
<Paragraph
|
||||||
|
@ -93,7 +93,6 @@ const ProviderInfo = ({ connected, provider, userAddress, network }: ProviderInf
|
||||||
identiconSize="xs"
|
identiconSize="xs"
|
||||||
textColor={addressColor}
|
textColor={addressColor}
|
||||||
textSize="sm"
|
textSize="sm"
|
||||||
network={network}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Text size="md" color={addressColor}>
|
<Text size="md" color={addressColor}>
|
||||||
|
@ -107,7 +106,7 @@ const ProviderInfo = ({ connected, provider, userAddress, network }: ProviderInf
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
<Col className={classes.networkLabel} layout="column" start="sm">
|
<Col className={classes.networkLabel} layout="column" start="sm">
|
||||||
<NetworkLabel network={network} />
|
<NetworkLabel />
|
||||||
</Col>
|
</Col>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
import { withStyles } from '@material-ui/core/styles'
|
import { withStyles } from '@material-ui/core/styles'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
import CircleDot from 'src/components/AppLayout/Header/components/CircleDot'
|
|
||||||
|
|
||||||
import Col from 'src/components/layout/Col'
|
import Col from 'src/components/layout/Col'
|
||||||
import Paragraph from 'src/components/layout/Paragraph'
|
import Paragraph from 'src/components/layout/Paragraph'
|
||||||
import { sm } from 'src/theme/variables'
|
import { sm } from 'src/theme/variables'
|
||||||
|
import { KeyRing } from 'src/components/AppLayout/Header/components/KeyRing'
|
||||||
|
|
||||||
const styles = () => ({
|
const styles = () => ({
|
||||||
network: {
|
network: {
|
||||||
|
@ -27,7 +26,7 @@ const styles = () => ({
|
||||||
|
|
||||||
const ProviderDisconnected = ({ classes }) => (
|
const ProviderDisconnected = ({ classes }) => (
|
||||||
<>
|
<>
|
||||||
<CircleDot circleSize={35} dotRight={11} dotSize={16} dotTop={24} keySize={17} mode="error" />
|
<KeyRing circleSize={35} dotRight={11} dotSize={16} dotTop={24} keySize={17} mode="error" />
|
||||||
<Col className={classes.account} end="sm" layout="column" middle="xs">
|
<Col className={classes.account} end="sm" layout="column" middle="xs">
|
||||||
<Paragraph
|
<Paragraph
|
||||||
className={classes.network}
|
className={classes.network}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { useSelector, useDispatch } from 'react-redux'
|
||||||
|
|
||||||
import Layout from './components/Layout'
|
import Layout from './components/Layout'
|
||||||
import ConnectDetails from './components/ProviderDetails/ConnectDetails'
|
import ConnectDetails from './components/ProviderDetails/ConnectDetails'
|
||||||
import UserDetails from './components/ProviderDetails/UserDetails'
|
import { UserDetails } from './components/ProviderDetails/UserDetails'
|
||||||
import ProviderAccessible from './components/ProviderInfo/ProviderAccessible'
|
import ProviderAccessible from './components/ProviderInfo/ProviderAccessible'
|
||||||
import ProviderDisconnected from './components/ProviderInfo/ProviderDisconnected'
|
import ProviderDisconnected from './components/ProviderInfo/ProviderDisconnected'
|
||||||
import {
|
import {
|
||||||
|
@ -54,7 +54,7 @@ const HeaderComponent = (): React.ReactElement => {
|
||||||
return <ProviderDisconnected />
|
return <ProviderDisconnected />
|
||||||
}
|
}
|
||||||
|
|
||||||
return <ProviderAccessible connected={available} provider={provider} network={network} userAddress={userAddress} />
|
return <ProviderAccessible connected={available} provider={provider} userAddress={userAddress} />
|
||||||
}
|
}
|
||||||
|
|
||||||
const getProviderDetailsBased = () => {
|
const getProviderDetailsBased = () => {
|
||||||
|
|
|
@ -8,11 +8,13 @@ import {
|
||||||
Identicon,
|
Identicon,
|
||||||
Button,
|
Button,
|
||||||
CopyToClipboardBtn,
|
CopyToClipboardBtn,
|
||||||
EtherscanButton,
|
ExplorerButton,
|
||||||
} from '@gnosis.pm/safe-react-components'
|
} from '@gnosis.pm/safe-react-components'
|
||||||
|
|
||||||
import { getNetwork } from 'src/config'
|
|
||||||
import FlexSpacer from 'src/components/FlexSpacer'
|
import FlexSpacer from 'src/components/FlexSpacer'
|
||||||
|
import { getExplorerInfo, getNetworkInfo } from 'src/config'
|
||||||
|
import { NetworkSettings } from 'src/config/networks/network.d'
|
||||||
|
import { border, fontColor } from 'src/theme/variables'
|
||||||
|
|
||||||
export const TOGGLE_SIDEBAR_BTN_TESTID = 'TOGGLE_SIDEBAR_BTN'
|
export const TOGGLE_SIDEBAR_BTN_TESTID = 'TOGGLE_SIDEBAR_BTN'
|
||||||
|
|
||||||
|
@ -46,6 +48,19 @@ const StyledButton = styled(Button)`
|
||||||
margin: 0 4px 0 0;
|
margin: 0 4px 0 0;
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
type StyledTextLabelProps = {
|
||||||
|
networkInfo: NetworkSettings
|
||||||
|
}
|
||||||
|
|
||||||
|
const StyledTextLabel = styled(Text)`
|
||||||
|
margin: -8px 0 4px -8px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
color: ${(props: StyledTextLabelProps) => props.networkInfo?.textColor ?? fontColor};
|
||||||
|
background-color: ${(props: StyledTextLabelProps) => props.networkInfo?.backgroundColor ?? border};
|
||||||
|
`
|
||||||
const StyledEthHashInfo = styled(EthHashInfo)`
|
const StyledEthHashInfo = styled(EthHashInfo)`
|
||||||
p {
|
p {
|
||||||
color: ${({ theme }) => theme.colors.placeHolder};
|
color: ${({ theme }) => theme.colors.placeHolder};
|
||||||
|
@ -110,43 +125,50 @@ const SafeHeader = ({
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
const explorerUrl = getExplorerInfo(address)
|
||||||
|
const networkInfo = getNetworkInfo()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<>
|
||||||
<IdenticonContainer>
|
<StyledTextLabel size="sm" networkInfo={networkInfo}>
|
||||||
<FlexSpacer />
|
{networkInfo.label}
|
||||||
<Identicon address={address} size="lg" />
|
</StyledTextLabel>
|
||||||
<UnStyledButton onClick={onToggleSafeList} data-testid={TOGGLE_SIDEBAR_BTN_TESTID}>
|
<Container>
|
||||||
<Icon size="md" type="circleDropdown" />
|
<IdenticonContainer>
|
||||||
</UnStyledButton>
|
<FlexSpacer />
|
||||||
</IdenticonContainer>
|
<Identicon address={address} size="lg" />
|
||||||
|
<UnStyledButton onClick={onToggleSafeList} data-testid={TOGGLE_SIDEBAR_BTN_TESTID}>
|
||||||
|
<Icon size="md" type="circleDropdown" />
|
||||||
|
</UnStyledButton>
|
||||||
|
</IdenticonContainer>
|
||||||
|
|
||||||
<Text size="xl">{safeName}</Text>
|
<Text size="xl">{safeName}</Text>
|
||||||
<StyledEthHashInfo hash={address} shortenHash={4} textSize="sm" />
|
<StyledEthHashInfo hash={address} shortenHash={4} textSize="sm" />
|
||||||
<IconContainer>
|
<IconContainer>
|
||||||
<UnStyledButton onClick={onReceiveClick}>
|
<UnStyledButton onClick={onReceiveClick}>
|
||||||
<Icon size="sm" type="qrCode" tooltip="Show QR" />
|
<Icon size="sm" type="qrCode" tooltip="Show QR" />
|
||||||
</UnStyledButton>
|
</UnStyledButton>
|
||||||
<CopyToClipboardBtn textToCopy={address} />
|
<CopyToClipboardBtn textToCopy={address} />
|
||||||
<EtherscanButton value={address} network={getNetwork()} />
|
<ExplorerButton explorerUrl={explorerUrl} />
|
||||||
</IconContainer>
|
</IconContainer>
|
||||||
|
|
||||||
{granted ? null : (
|
{granted ? null : (
|
||||||
<StyledLabel>
|
<StyledLabel>
|
||||||
<Text size="sm" color="white">
|
<Text size="sm" color="white">
|
||||||
READ ONLY
|
READ ONLY
|
||||||
|
</Text>
|
||||||
|
</StyledLabel>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<StyledText size="xl">{balance}</StyledText>
|
||||||
|
<StyledButton size="md" disabled={!granted} color="primary" variant="contained" onClick={onNewTransactionClick}>
|
||||||
|
<FixedIcon type="arrowSentWhite" />
|
||||||
|
<Text size="lg" color="white">
|
||||||
|
New Transaction
|
||||||
</Text>
|
</Text>
|
||||||
</StyledLabel>
|
</StyledButton>
|
||||||
)}
|
</Container>
|
||||||
|
</>
|
||||||
<StyledText size="xl">{balance}</StyledText>
|
|
||||||
<StyledButton size="md" disabled={!granted} color="primary" variant="contained" onClick={onNewTransactionClick}>
|
|
||||||
<FixedIcon type="arrowSentWhite" />
|
|
||||||
<Text size="lg" color="white">
|
|
||||||
New Transaction
|
|
||||||
</Text>
|
|
||||||
</StyledButton>
|
|
||||||
</Container>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,8 +4,13 @@ import { useRouteMatch } from 'react-router-dom'
|
||||||
import { ListItemType } from 'src/components/List'
|
import { ListItemType } from 'src/components/List'
|
||||||
import ListIcon from 'src/components/List/ListIcon'
|
import ListIcon from 'src/components/List/ListIcon'
|
||||||
import { SAFELIST_ADDRESS } from 'src/routes/routes'
|
import { SAFELIST_ADDRESS } from 'src/routes/routes'
|
||||||
|
import { FEATURES } from 'src/config/networks/network.d'
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
import { safeFeaturesEnabledSelector } from 'src/logic/safe/store/selectors'
|
||||||
|
|
||||||
const useSidebarItems = (): ListItemType[] => {
|
const useSidebarItems = (): ListItemType[] => {
|
||||||
|
const featuresEnabled = useSelector(safeFeaturesEnabledSelector)
|
||||||
|
const safeAppsEnabled = Boolean(featuresEnabled?.includes(FEATURES.SAFE_APPS))
|
||||||
const matchSafe = useRouteMatch({ path: `${SAFELIST_ADDRESS}`, strict: false })
|
const matchSafe = useRouteMatch({ path: `${SAFELIST_ADDRESS}`, strict: false })
|
||||||
const matchSafeWithAddress = useRouteMatch<{ safeAddress: string }>({ path: `${SAFELIST_ADDRESS}/:safeAddress` })
|
const matchSafeWithAddress = useRouteMatch<{ safeAddress: string }>({ path: `${SAFELIST_ADDRESS}/:safeAddress` })
|
||||||
const matchSafeWithAction = useRouteMatch({ path: `${SAFELIST_ADDRESS}/:safeAddress/:safeAction` }) as {
|
const matchSafeWithAction = useRouteMatch({ path: `${SAFELIST_ADDRESS}/:safeAddress/:safeAction` }) as {
|
||||||
|
@ -13,11 +18,30 @@ const useSidebarItems = (): ListItemType[] => {
|
||||||
params: Record<string, string>
|
params: Record<string, string>
|
||||||
}
|
}
|
||||||
|
|
||||||
const sidebarItems = useMemo((): ListItemType[] => {
|
return useMemo((): ListItemType[] => {
|
||||||
if (!matchSafe || !matchSafeWithAddress) {
|
if (!matchSafe || !matchSafeWithAddress) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const settingsItem = {
|
||||||
|
label: 'Settings',
|
||||||
|
icon: <ListIcon type="settings" />,
|
||||||
|
selected: matchSafeWithAction?.params.safeAction === 'settings',
|
||||||
|
href: `${matchSafeWithAddress?.url}/settings`,
|
||||||
|
}
|
||||||
|
|
||||||
|
const safeSidebar = safeAppsEnabled
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
label: 'Apps',
|
||||||
|
icon: <ListIcon type="apps" />,
|
||||||
|
selected: matchSafeWithAction?.params.safeAction === 'apps',
|
||||||
|
href: `${matchSafeWithAddress?.url}/apps`,
|
||||||
|
},
|
||||||
|
settingsItem,
|
||||||
|
]
|
||||||
|
: [settingsItem]
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
label: 'ASSETS',
|
label: 'ASSETS',
|
||||||
|
@ -37,22 +61,9 @@ const useSidebarItems = (): ListItemType[] => {
|
||||||
selected: matchSafeWithAction?.params.safeAction === 'address-book',
|
selected: matchSafeWithAction?.params.safeAction === 'address-book',
|
||||||
href: `${matchSafeWithAddress?.url}/address-book`,
|
href: `${matchSafeWithAddress?.url}/address-book`,
|
||||||
},
|
},
|
||||||
{
|
...safeSidebar,
|
||||||
label: 'Apps',
|
|
||||||
icon: <ListIcon type="apps" />,
|
|
||||||
selected: matchSafeWithAction?.params.safeAction === 'apps',
|
|
||||||
href: `${matchSafeWithAddress?.url}/apps`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Settings',
|
|
||||||
icon: <ListIcon type="settings" />,
|
|
||||||
selected: matchSafeWithAction?.params.safeAction === 'settings',
|
|
||||||
href: `${matchSafeWithAddress?.url}/settings`,
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
}, [matchSafe, matchSafeWithAction, matchSafeWithAddress])
|
}, [matchSafe, matchSafeWithAction, matchSafeWithAddress, safeAppsEnabled])
|
||||||
|
|
||||||
return sidebarItems
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export { useSidebarItems }
|
export { useSidebarItems }
|
||||||
|
|
|
@ -3,15 +3,16 @@ import React from 'react'
|
||||||
|
|
||||||
import Button from 'src/components/layout/Button'
|
import Button from 'src/components/layout/Button'
|
||||||
import { getNetworkId } from 'src/config'
|
import { getNetworkId } from 'src/config'
|
||||||
|
import { ETHEREUM_NETWORK } from 'src/config/networks/network.d'
|
||||||
import { getWeb3, setWeb3 } from 'src/logic/wallets/getWeb3'
|
import { getWeb3, setWeb3 } from 'src/logic/wallets/getWeb3'
|
||||||
import { fetchProvider } from 'src/logic/wallets/store/actions'
|
import { fetchProvider } from 'src/logic/wallets/store/actions'
|
||||||
import transactionDataCheck from 'src/logic/wallets/transactionDataCheck'
|
import transactionDataCheck from 'src/logic/wallets/transactionDataCheck'
|
||||||
import { getSupportedWallets } from 'src/logic/wallets/utils/walletList'
|
import { getSupportedWallets } from 'src/logic/wallets/utils/walletList'
|
||||||
import { store } from 'src/store'
|
import { store } from 'src/store'
|
||||||
|
import { BLOCKNATIVE_KEY } from 'src/utils/constants'
|
||||||
|
|
||||||
const isMainnet = process.env.REACT_APP_NETWORK === 'mainnet'
|
const networkId = getNetworkId()
|
||||||
|
const BLOCKNATIVE_API_KEY = BLOCKNATIVE_KEY[networkId] ?? BLOCKNATIVE_KEY[ETHEREUM_NETWORK.RINKEBY]
|
||||||
const BLOCKNATIVE_API_KEY = isMainnet ? process.env.REACT_APP_BLOCKNATIVE_KEY : '7fbb9cee-7e97-4436-8770-8b29a9a8814c'
|
|
||||||
|
|
||||||
let lastUsedAddress = ''
|
let lastUsedAddress = ''
|
||||||
let providerName
|
let providerName
|
||||||
|
@ -20,7 +21,7 @@ const wallets = getSupportedWallets()
|
||||||
|
|
||||||
export const onboard = Onboard({
|
export const onboard = Onboard({
|
||||||
dappId: BLOCKNATIVE_API_KEY,
|
dappId: BLOCKNATIVE_API_KEY,
|
||||||
networkId: getNetworkId(),
|
networkId: networkId,
|
||||||
subscriptions: {
|
subscriptions: {
|
||||||
wallet: (wallet) => {
|
wallet: (wallet) => {
|
||||||
if (wallet.provider) {
|
if (wallet.provider) {
|
||||||
|
|
|
@ -6,8 +6,8 @@ import React from 'react'
|
||||||
import EtherscanOpenIcon from './img/etherscan-open.svg'
|
import EtherscanOpenIcon from './img/etherscan-open.svg'
|
||||||
|
|
||||||
import Img from 'src/components/layout/Img'
|
import Img from 'src/components/layout/Img'
|
||||||
import { getEtherScanLink } from 'src/logic/wallets/getWeb3'
|
|
||||||
import { xs } from 'src/theme/variables'
|
import { xs } from 'src/theme/variables'
|
||||||
|
import { getExplorerInfo } from 'src/config'
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
const useStyles = makeStyles({
|
||||||
container: {
|
container: {
|
||||||
|
@ -30,26 +30,23 @@ const useStyles = makeStyles({
|
||||||
interface EtherscanBtnProps {
|
interface EtherscanBtnProps {
|
||||||
className?: string
|
className?: string
|
||||||
increaseZindex?: boolean
|
increaseZindex?: boolean
|
||||||
type: 'tx' | 'address'
|
|
||||||
value: string
|
value: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const EtherscanBtn = ({
|
const EtherscanBtn = ({ className = '', increaseZindex = false, value }: EtherscanBtnProps): React.ReactElement => {
|
||||||
className = '',
|
|
||||||
increaseZindex = false,
|
|
||||||
type,
|
|
||||||
value,
|
|
||||||
}: EtherscanBtnProps): React.ReactElement => {
|
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
const customClasses = increaseZindex ? { popper: classes.increasedPopperZindex } : {}
|
const customClasses = increaseZindex ? { popper: classes.increasedPopperZindex } : {}
|
||||||
|
|
||||||
|
const explorerInfo = getExplorerInfo(value)
|
||||||
|
const { url } = explorerInfo()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip classes={customClasses} placement="top" title="Show details on Etherscan">
|
<Tooltip classes={customClasses} placement="top" title="Show details on Etherscan">
|
||||||
<a
|
<a
|
||||||
aria-label="Show details on Etherscan"
|
aria-label="Show details on Etherscan"
|
||||||
className={cn(classes.container, className)}
|
className={cn(classes.container, className)}
|
||||||
onClick={(event) => event.stopPropagation()}
|
onClick={(event) => event.stopPropagation()}
|
||||||
href={getEtherScanLink(type, value)}
|
href={url}
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
|
|
|
@ -17,11 +17,10 @@ interface EtherscanLinkProps {
|
||||||
className?: string
|
className?: string
|
||||||
cut?: number
|
cut?: number
|
||||||
knownAddress?: boolean
|
knownAddress?: boolean
|
||||||
type: 'tx' | 'address'
|
|
||||||
value: string
|
value: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const EtherscanLink = ({ className, cut, knownAddress, type, value }: EtherscanLinkProps): React.ReactElement => {
|
const EtherscanLink = ({ className, cut, knownAddress, value }: EtherscanLinkProps): React.ReactElement => {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -30,7 +29,7 @@ const EtherscanLink = ({ className, cut, knownAddress, type, value }: EtherscanL
|
||||||
{cut ? shortVersionOf(value, cut) : value}
|
{cut ? shortVersionOf(value, cut) : value}
|
||||||
</Span>
|
</Span>
|
||||||
<CopyBtn className={cn(classes.button, classes.firstButton)} content={value} />
|
<CopyBtn className={cn(classes.button, classes.firstButton)} content={value} />
|
||||||
<EtherscanBtn className={classes.button} type={type} value={value} />
|
<EtherscanBtn className={classes.button} value={value} />
|
||||||
{knownAddress !== undefined ? <EllipsisTransactionDetails address={value} knownAddress={knownAddress} /> : null}
|
{knownAddress !== undefined ? <EllipsisTransactionDetails address={value} knownAddress={knownAddress} /> : null}
|
||||||
</Block>
|
</Block>
|
||||||
)
|
)
|
||||||
|
|
|
@ -3,7 +3,7 @@ import styled from 'styled-components'
|
||||||
export const Wrapper = styled.div`
|
export const Wrapper = styled.div`
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 245px auto;
|
grid-template-columns: 245px auto;
|
||||||
min-height: 560px;
|
min-height: 75vh;
|
||||||
.background {
|
.background {
|
||||||
box-shadow: 1px 2px 10px 0 rgba(212, 212, 211, 0.59);
|
box-shadow: 1px 2px 10px 0 rgba(212, 212, 211, 0.59);
|
||||||
background-color: white;
|
background-color: white;
|
||||||
|
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,62 +1,22 @@
|
||||||
import MuiList from '@material-ui/core/List'
|
import MuiList from '@material-ui/core/List'
|
||||||
import ListItem from '@material-ui/core/ListItem'
|
import ListItem from '@material-ui/core/ListItem'
|
||||||
import { makeStyles } from '@material-ui/core/styles'
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
import { EthHashInfo, Icon, Text, ButtonLink } from '@gnosis.pm/safe-react-components'
|
import { Icon } from '@gnosis.pm/safe-react-components'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import { SafeRecord } from 'src/logic/safe/store/models/safe'
|
import { SafeRecord } from 'src/logic/safe/store/models/safe'
|
||||||
import { DefaultSafe } from 'src/routes/safe/store/reducer/types/safe'
|
import { DefaultSafe } from 'src/routes/safe/store/reducer/types/safe'
|
||||||
import { SetDefaultSafe } from 'src/logic/safe/store/actions/setDefaultSafe'
|
import { SetDefaultSafe } from 'src/logic/safe/store/actions/setDefaultSafe'
|
||||||
import { getNetwork } from 'src/config'
|
|
||||||
import DefaultBadge from './DefaultBadge'
|
|
||||||
import Hairline from 'src/components/layout/Hairline'
|
import Hairline from 'src/components/layout/Hairline'
|
||||||
import Link from 'src/components/layout/Link'
|
import Link from 'src/components/layout/Link'
|
||||||
import { formatAmount } from 'src/logic/tokens/utils/formatAmount'
|
|
||||||
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
||||||
import { SAFELIST_ADDRESS } from 'src/routes/routes'
|
import { SAFELIST_ADDRESS } from 'src/routes/routes'
|
||||||
|
import { AddressWrapper } from './AddresWrapper'
|
||||||
export const SIDEBAR_SAFELIST_ROW_TESTID = 'SIDEBAR_SAFELIST_ROW_TESTID'
|
export const SIDEBAR_SAFELIST_ROW_TESTID = 'SIDEBAR_SAFELIST_ROW_TESTID'
|
||||||
|
|
||||||
const StyledIcon = styled(Icon)`
|
const StyledIcon = styled(Icon)`
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
`
|
`
|
||||||
const AddressWrapper = styled.div`
|
|
||||||
display: flex;
|
|
||||||
padding: 5px 0;
|
|
||||||
width: 100%;
|
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
> nth-child(2) {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const StyledButtonLink = styled(ButtonLink)`
|
|
||||||
visibility: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
`
|
|
||||||
|
|
||||||
const AddressDetails = styled.div`
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
width: 175px;
|
|
||||||
|
|
||||||
div {
|
|
||||||
margin-left: 0px;
|
|
||||||
padding: 5px 20px;
|
|
||||||
|
|
||||||
img {
|
|
||||||
margin-right: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin-top: 3px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
const useStyles = makeStyles({
|
||||||
list: {
|
list: {
|
||||||
|
@ -107,34 +67,7 @@ const SafeList = ({ currentSafe, defaultSafe, onSafeClick, safes, setDefaultSafe
|
||||||
) : (
|
) : (
|
||||||
<div className={classes.noIcon}>placeholder</div>
|
<div className={classes.noIcon}>placeholder</div>
|
||||||
)}
|
)}
|
||||||
|
<AddressWrapper safe={safe} defaultSafe={defaultSafe} setDefaultSafe={setDefaultSafe} />
|
||||||
<AddressWrapper>
|
|
||||||
<EthHashInfo
|
|
||||||
hash={safe.address}
|
|
||||||
name={safe.name}
|
|
||||||
showIdenticon
|
|
||||||
shortenHash={4}
|
|
||||||
network={getNetwork()}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<AddressDetails>
|
|
||||||
<Text size="xl">{`${formatAmount(safe.ethBalance)} ETH`}</Text>
|
|
||||||
{sameAddress(defaultSafe, safe.address) ? (
|
|
||||||
<DefaultBadge />
|
|
||||||
) : (
|
|
||||||
<StyledButtonLink
|
|
||||||
className="safeListMakeDefaultButton"
|
|
||||||
textSize="sm"
|
|
||||||
onClick={() => {
|
|
||||||
setDefaultSafe(safe.address)
|
|
||||||
}}
|
|
||||||
color="primary"
|
|
||||||
>
|
|
||||||
Make default
|
|
||||||
</StyledButtonLink>
|
|
||||||
)}
|
|
||||||
</AddressDetails>
|
|
||||||
</AddressWrapper>
|
|
||||||
</ListItem>
|
</ListItem>
|
||||||
</Link>
|
</Link>
|
||||||
<Hairline />
|
<Hairline />
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { Validator, composeValidators, mustBeEthereumAddress, required } from 's
|
||||||
import { trimSpaces } from 'src/utils/strings'
|
import { trimSpaces } from 'src/utils/strings'
|
||||||
import { getAddressFromENS } from 'src/logic/wallets/getWeb3'
|
import { getAddressFromENS } from 'src/logic/wallets/getWeb3'
|
||||||
import { isValidEnsName } from 'src/logic/wallets/ethAddresses'
|
import { isValidEnsName } from 'src/logic/wallets/ethAddresses'
|
||||||
|
import { checksumAddress } from 'src/utils/checksumAddress'
|
||||||
|
|
||||||
// an idea for second field was taken from here
|
// an idea for second field was taken from here
|
||||||
// https://github.com/final-form/react-final-form-listeners/blob/master/src/OnBlur.js
|
// https://github.com/final-form/react-final-form-listeners/blob/master/src/OnBlur.js
|
||||||
|
@ -56,11 +57,15 @@ const AddressInput = ({
|
||||||
if (isValidEnsName(address)) {
|
if (isValidEnsName(address)) {
|
||||||
try {
|
try {
|
||||||
const resolverAddr = await getAddressFromENS(address)
|
const resolverAddr = await getAddressFromENS(address)
|
||||||
fieldMutator(resolverAddr)
|
const formattedAddress = checksumAddress(resolverAddr)
|
||||||
|
fieldMutator(formattedAddress)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to resolve address for ENS name: ', err)
|
console.error('Failed to resolve address for ENS name: ', err)
|
||||||
}
|
}
|
||||||
} else fieldMutator(address)
|
} else {
|
||||||
|
const formattedAddress = checksumAddress(address)
|
||||||
|
fieldMutator(formattedAddress)
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
</OnChange>
|
</OnChange>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -1,11 +1,17 @@
|
||||||
import classNames from 'classnames/bind'
|
import classNames from 'classnames/bind'
|
||||||
import React from 'react'
|
import React, { ReactElement, ImgHTMLAttributes } from 'react'
|
||||||
|
|
||||||
import styles from './index.module.scss'
|
import styles from './index.module.scss'
|
||||||
|
|
||||||
const cx = classNames.bind(styles)
|
const cx = classNames.bind(styles)
|
||||||
|
|
||||||
const Img: any = ({ alt, bordered, className, fullwidth, style, testId = '', ...props }) => {
|
type ImgProps = ImgHTMLAttributes<HTMLImageElement> & {
|
||||||
|
bordered?: boolean
|
||||||
|
fullwidth?: boolean
|
||||||
|
testId?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const Img = ({ alt, bordered, className, fullwidth, style, testId = '', ...props }: ImgProps): ReactElement => {
|
||||||
const classes = cx(styles.img, { fullwidth, bordered }, className)
|
const classes = cx(styles.img, { fullwidth, bordered }, className)
|
||||||
|
|
||||||
return <img alt={alt} className={classes} data-testid={testId} style={style} {...props} />
|
return <img alt={alt} className={classes} data-testid={testId} style={style} {...props} />
|
||||||
|
|
|
@ -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)
|
||||||
|
})
|
||||||
|
})
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -1,103 +1,165 @@
|
||||||
import { checksumAddress } from 'src/utils/checksumAddress';
|
import networks from 'src/config/networks'
|
||||||
|
import { EnvironmentSettings, ETHEREUM_NETWORK, NetworkSettings, SafeFeatures } from 'src/config/networks/network.d'
|
||||||
|
import { APP_ENV, ETHERSCAN_API_KEY, GOOGLE_ANALYTICS_ID, INFURA_TOKEN, NETWORK, NODE_ENV } from 'src/utils/constants'
|
||||||
import { ensureOnce } from 'src/utils/singleton'
|
import { ensureOnce } from 'src/utils/singleton'
|
||||||
import { ETHEREUM_NETWORK } from 'src/logic/wallets/getWeb3'
|
import memoize from 'lodash.memoize'
|
||||||
import {
|
|
||||||
RELAY_API_URL,
|
|
||||||
SIGNATURES_VIA_METAMASK,
|
|
||||||
TX_SERVICE_HOST,
|
|
||||||
SAFE_APPS_URL
|
|
||||||
} from 'src/config/names'
|
|
||||||
import devConfig from './development'
|
|
||||||
import testConfig from './testing'
|
|
||||||
import stagingConfig from './staging'
|
|
||||||
import prodConfig from './production'
|
|
||||||
import mainnetDevConfig from './development-mainnet'
|
|
||||||
import mainnetProdConfig from './production-mainnet'
|
|
||||||
import mainnetStagingConfig from './staging-mainnet'
|
|
||||||
|
|
||||||
const configuration = () => {
|
export const getNetworkId = (): ETHEREUM_NETWORK => ETHEREUM_NETWORK[NETWORK]
|
||||||
if (process.env.NODE_ENV === 'test') {
|
|
||||||
return testConfig
|
export const getNetworkName = (): string => ETHEREUM_NETWORK[getNetworkId()]
|
||||||
|
|
||||||
|
const getCurrentEnvironment = (): string => {
|
||||||
|
switch (NODE_ENV) {
|
||||||
|
case 'test': {
|
||||||
|
return 'test'
|
||||||
|
}
|
||||||
|
case 'production': {
|
||||||
|
return APP_ENV === 'production' ? 'production' : 'staging'
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return 'dev'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type NetworkSpecificConfiguration = EnvironmentSettings & {
|
||||||
|
network: NetworkSettings,
|
||||||
|
disabledFeatures?: SafeFeatures,
|
||||||
|
}
|
||||||
|
|
||||||
|
const configuration = (): NetworkSpecificConfiguration => {
|
||||||
|
const currentEnvironment = getCurrentEnvironment()
|
||||||
|
|
||||||
|
// special case for test environment
|
||||||
|
if (currentEnvironment === 'test') {
|
||||||
|
const configFile = networks.local
|
||||||
|
|
||||||
|
return {
|
||||||
|
...configFile.environment.production,
|
||||||
|
network: configFile.network,
|
||||||
|
disabledFeatures: configFile.disabledFeatures,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'production') {
|
// lookup the config file based on the network specified in the NETWORK variable
|
||||||
if (process.env.REACT_APP_NETWORK === 'mainnet') {
|
const configFile = networks[getNetworkName().toLowerCase()]
|
||||||
return process.env.REACT_APP_ENV === 'production'
|
// defaults to 'production' as it's the only environment that is required for the network configs
|
||||||
? mainnetProdConfig
|
const networkBaseConfig = configFile.environment[currentEnvironment] ?? configFile.environment.production
|
||||||
: mainnetStagingConfig
|
|
||||||
|
return {
|
||||||
|
...networkBaseConfig,
|
||||||
|
network: configFile.network,
|
||||||
|
disabledFeatures: configFile.disabledFeatures,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getConfig: () => NetworkSpecificConfiguration = ensureOnce(configuration)
|
||||||
|
|
||||||
|
export const getTxServiceUrl = (): string => getConfig()?.txServiceUrl
|
||||||
|
|
||||||
|
export const getRelayUrl = (): string | undefined => getConfig()?.relayApiUrl
|
||||||
|
|
||||||
|
export const getGnosisSafeAppsUrl = (): string => getConfig()?.safeAppsUrl
|
||||||
|
|
||||||
|
export const getRpcServiceUrl = (): string => {
|
||||||
|
const usesInfuraRPC = [ETHEREUM_NETWORK.MAINNET, ETHEREUM_NETWORK.RINKEBY].includes(getNetworkId())
|
||||||
|
|
||||||
|
if (usesInfuraRPC) {
|
||||||
|
return `${getConfig()?.rpcServiceUrl}/${INFURA_TOKEN}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return getConfig()?.rpcServiceUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getNetworkExplorerInfo = (): { name: string; url: string; apiUrl: string } => ({
|
||||||
|
name: getConfig()?.networkExplorerName,
|
||||||
|
url: getConfig()?.networkExplorerUrl,
|
||||||
|
apiUrl: getConfig()?.networkExplorerApiUrl,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const getNetworkConfigDisabledFeatures = (): SafeFeatures => getConfig()?.disabledFeatures || []
|
||||||
|
|
||||||
|
export const getNetworkInfo = (): NetworkSettings => getConfig()?.network
|
||||||
|
|
||||||
|
export const getTxServiceUriFrom = (safeAddress: string) => `/safes/${safeAddress}/transactions/`
|
||||||
|
|
||||||
|
export const getIncomingTxServiceUriTo = (safeAddress: string) => `/safes/${safeAddress}/incoming-transfers/`
|
||||||
|
|
||||||
|
export const getAllTransactionsUriFrom = (safeAddress: string) => `/safes/${safeAddress}/all-transactions/`
|
||||||
|
|
||||||
|
export const getSafeCreationTxUri = (safeAddress: string) => `/safes/${safeAddress}/creation/`
|
||||||
|
|
||||||
|
export const getGoogleAnalyticsTrackingID = (): string => GOOGLE_ANALYTICS_ID
|
||||||
|
|
||||||
|
const fetchContractABI = memoize(
|
||||||
|
async (url: string, contractAddress: string, apiKey?: string) => {
|
||||||
|
let params: Record<string, string> = {
|
||||||
|
module: 'contract',
|
||||||
|
action: 'getAbi',
|
||||||
|
address: contractAddress,
|
||||||
}
|
}
|
||||||
|
|
||||||
return process.env.REACT_APP_ENV === 'production'
|
if (apiKey) {
|
||||||
? prodConfig
|
params = { ...params, apiKey }
|
||||||
: stagingConfig
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`${url}?${new URLSearchParams(params)}`)
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return { status: 0, result: [] }
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json()
|
||||||
|
},
|
||||||
|
(url, contractAddress) => `${url}_${contractAddress}`,
|
||||||
|
)
|
||||||
|
|
||||||
|
const getNetworkExplorerApiKey = (networkExplorerName: string): string | undefined=> {
|
||||||
|
switch (networkExplorerName.toLowerCase()) {
|
||||||
|
case 'etherscan': {
|
||||||
|
return ETHERSCAN_API_KEY
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return process.env.REACT_APP_NETWORK === 'mainnet'
|
|
||||||
? mainnetDevConfig
|
|
||||||
: devConfig
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getNetwork = () =>
|
export const getContractABI = async (contractAddress: string) =>{
|
||||||
process.env.REACT_APP_NETWORK === 'mainnet'
|
const { apiUrl, name } = getNetworkExplorerInfo()
|
||||||
? ETHEREUM_NETWORK.MAINNET
|
|
||||||
: ETHEREUM_NETWORK.RINKEBY
|
|
||||||
|
|
||||||
export const getNetworkId = () =>
|
const apiKey = getNetworkExplorerApiKey(name)
|
||||||
process.env.REACT_APP_NETWORK === 'mainnet' ? 1 : 4
|
|
||||||
|
|
||||||
const getConfig = ensureOnce(configuration)
|
try {
|
||||||
|
const { result, status } = await fetchContractABI(apiUrl, contractAddress, apiKey)
|
||||||
|
|
||||||
export const getTxServiceHost = () => {
|
if (status === '0') {
|
||||||
const config = getConfig()
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
return config[TX_SERVICE_HOST]
|
return result
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to retrieve ABI', e)
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getTxServiceUriFrom = (safeAddress) =>
|
export type BlockScanInfo = () => {
|
||||||
`safes/${safeAddress}/transactions/`
|
alt: string
|
||||||
|
url: string
|
||||||
export const getIncomingTxServiceUriTo = (safeAddress) =>
|
|
||||||
`safes/${safeAddress}/incoming-transfers/`
|
|
||||||
|
|
||||||
export const getAllTransactionsUriFrom = (safeAddress: string): string =>
|
|
||||||
`safes/${safeAddress}/all-transactions/`
|
|
||||||
|
|
||||||
export const getSafeCreationTxUri = (safeAddress) => `safes/${safeAddress}/creation/`
|
|
||||||
|
|
||||||
export const getRelayUrl = () => getConfig()[RELAY_API_URL]
|
|
||||||
|
|
||||||
export const signaturesViaMetamask = () => {
|
|
||||||
const config = getConfig()
|
|
||||||
|
|
||||||
return config[SIGNATURES_VIA_METAMASK]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getGnosisSafeAppsUrl = () => {
|
export const getExplorerInfo = (hash: string): BlockScanInfo => {
|
||||||
const config = getConfig()
|
const { name, url } = getNetworkExplorerInfo()
|
||||||
|
const networkInfo = getNetworkInfo()
|
||||||
|
|
||||||
return config[SAFE_APPS_URL]
|
switch (networkInfo.id) {
|
||||||
}
|
default: {
|
||||||
|
const type = hash.length > 42 ? 'tx' : 'address'
|
||||||
export const getGoogleAnalyticsTrackingID = () =>
|
return () => ({
|
||||||
getNetwork() === ETHEREUM_NETWORK.MAINNET
|
url: `${url}${type}/${hash}`,
|
||||||
? process.env.REACT_APP_GOOGLE_ANALYTICS_ID_MAINNET
|
alt: name || '',
|
||||||
: process.env.REACT_APP_GOOGLE_ANALYTICS_ID_RINKEBY
|
})
|
||||||
|
}
|
||||||
export const getIntercomId = () =>
|
}
|
||||||
process.env.REACT_APP_ENV === 'production'
|
|
||||||
? process.env.REACT_APP_INTERCOM_ID
|
|
||||||
: 'plssl1fl'
|
|
||||||
|
|
||||||
export const getExchangeRatesUrl = () => 'https://api.exchangeratesapi.io/latest'
|
|
||||||
|
|
||||||
export const getExchangeRatesUrlFallback = () => 'https://api.coinbase.com/v2/exchange-rates'
|
|
||||||
|
|
||||||
export const getSafeLastVersion = () => process.env.REACT_APP_LATEST_SAFE_VERSION || '1.1.1'
|
|
||||||
|
|
||||||
export const buildSafeCreationTxUrl = (safeAddress) => {
|
|
||||||
const host = getTxServiceHost()
|
|
||||||
const address = checksumAddress(safeAddress)
|
|
||||||
const base = getSafeCreationTxUri(address)
|
|
||||||
|
|
||||||
return `${host}${base}`
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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'
|
|
|
@ -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()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,11 @@
|
||||||
|
import local from './local'
|
||||||
|
import mainnet from './mainnet'
|
||||||
|
import rinkeby from './rinkeby'
|
||||||
|
import xdai from './xdai'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
local,
|
||||||
|
mainnet,
|
||||||
|
rinkeby,
|
||||||
|
xdai,
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
import EtherLogo from 'src/assets/icons/icon_etherTokens.svg'
|
||||||
|
import { EnvironmentSettings, ETHEREUM_NETWORK, NetworkConfig } from 'src/config/networks/network.d'
|
||||||
|
|
||||||
|
const baseConfig: EnvironmentSettings = {
|
||||||
|
txServiceUrl: 'http://localhost:8000/api/v1',
|
||||||
|
relayApiUrl: 'https://safe-relay.staging.gnosisdev.com/api/v1',
|
||||||
|
safeAppsUrl: 'http://localhost:3002',
|
||||||
|
gasPriceOracleUrl: 'https://ethgasstation.info/json/ethgasAPI.json',
|
||||||
|
rpcServiceUrl: 'http://localhost:4447',
|
||||||
|
networkExplorerName: 'Etherscan',
|
||||||
|
networkExplorerUrl: 'https://rinkeby.etherscan.io',
|
||||||
|
networkExplorerApiUrl: 'https://api-rinkeby.etherscan.io/api',
|
||||||
|
}
|
||||||
|
|
||||||
|
const local: NetworkConfig = {
|
||||||
|
environment: {
|
||||||
|
production: {
|
||||||
|
...baseConfig,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
network: {
|
||||||
|
id: ETHEREUM_NETWORK.LOCAL,
|
||||||
|
backgroundColor: '#E8673C',
|
||||||
|
textColor: '#ffffff',
|
||||||
|
label: 'LocalRPC',
|
||||||
|
isTestNet: true,
|
||||||
|
nativeCoin: {
|
||||||
|
address: '0x000',
|
||||||
|
name: 'Ether',
|
||||||
|
symbol: 'ETH',
|
||||||
|
decimals: 18,
|
||||||
|
logoUri: EtherLogo,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default local
|
|
@ -0,0 +1,45 @@
|
||||||
|
import EtherLogo from 'src/assets/icons/icon_etherTokens.svg'
|
||||||
|
import { EnvironmentSettings, ETHEREUM_NETWORK, NetworkConfig } from 'src/config/networks/network.d'
|
||||||
|
|
||||||
|
const baseConfig: EnvironmentSettings = {
|
||||||
|
txServiceUrl: 'https://safe-transaction.mainnet.staging.gnosisdev.com/api/v1',
|
||||||
|
safeAppsUrl: 'https://safe-apps.dev.gnosisdev.com',
|
||||||
|
gasPriceOracleUrl: 'https://ethgasstation.info/json/ethgasAPI.json',
|
||||||
|
rpcServiceUrl: 'https://mainnet.infura.io:443/v3',
|
||||||
|
networkExplorerName: 'Etherscan',
|
||||||
|
networkExplorerUrl: 'https://etherscan.io',
|
||||||
|
networkExplorerApiUrl: 'https://api.etherscan.io/api',
|
||||||
|
}
|
||||||
|
|
||||||
|
const mainnet: NetworkConfig = {
|
||||||
|
environment: {
|
||||||
|
dev: {
|
||||||
|
...baseConfig,
|
||||||
|
},
|
||||||
|
staging: {
|
||||||
|
...baseConfig,
|
||||||
|
safeAppsUrl: 'https://safe-apps.staging.gnosisdev.com',
|
||||||
|
},
|
||||||
|
production: {
|
||||||
|
...baseConfig,
|
||||||
|
txServiceUrl: 'https://safe-transaction.mainnet.gnosis.io/api/v1',
|
||||||
|
safeAppsUrl: 'https://apps.gnosis-safe.io',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
network: {
|
||||||
|
id: ETHEREUM_NETWORK.MAINNET,
|
||||||
|
backgroundColor: '#E8E7E6',
|
||||||
|
textColor: '#001428',
|
||||||
|
label: 'Mainnet',
|
||||||
|
isTestNet: false,
|
||||||
|
nativeCoin: {
|
||||||
|
address: '0x000',
|
||||||
|
name: 'Ether',
|
||||||
|
symbol: 'ETH',
|
||||||
|
decimals: 18,
|
||||||
|
logoUri: EtherLogo,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default mainnet
|
|
@ -0,0 +1,77 @@
|
||||||
|
// matches src/logic/tokens/store/model/token.ts `TokenProps` type
|
||||||
|
|
||||||
|
export enum FEATURES {
|
||||||
|
ERC721 = 'ERC721',
|
||||||
|
ERC1155 = 'ERC1155',
|
||||||
|
SAFE_APPS = 'SAFE_APPS',
|
||||||
|
CONTRACT_INTERACTION = 'CONTRACT_INTERACTION'
|
||||||
|
}
|
||||||
|
|
||||||
|
type Token = {
|
||||||
|
address: string
|
||||||
|
name: string
|
||||||
|
symbol: string
|
||||||
|
decimals: number
|
||||||
|
logoUri?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ETHEREUM_NETWORK {
|
||||||
|
MAINNET = 1,
|
||||||
|
MORDEN = 2,
|
||||||
|
ROPSTEN = 3,
|
||||||
|
RINKEBY = 4,
|
||||||
|
GOERLI = 5,
|
||||||
|
KOVAN = 42,
|
||||||
|
XDAI = 100,
|
||||||
|
ENERGY_WEB_CHAIN = 246,
|
||||||
|
VOLTA = 73799,
|
||||||
|
UNKNOWN = 0,
|
||||||
|
LOCAL = 4447,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NetworkSettings = {
|
||||||
|
// TODO: id now seems to be unnecessary
|
||||||
|
id: ETHEREUM_NETWORK,
|
||||||
|
backgroundColor: string,
|
||||||
|
textColor: string,
|
||||||
|
label: string,
|
||||||
|
isTestNet: boolean,
|
||||||
|
nativeCoin: Token,
|
||||||
|
}
|
||||||
|
|
||||||
|
// something around this to display or not some critical sections in the app, depending on the network support
|
||||||
|
// I listed the ones that may conflict with the network.
|
||||||
|
// If non is present, all the sections are available.
|
||||||
|
export type SafeFeatures = FEATURES[]
|
||||||
|
|
||||||
|
type GasPrice = {
|
||||||
|
gasPrice: number
|
||||||
|
gasPriceOracleUrl?: string
|
||||||
|
} | {
|
||||||
|
gasPrice?: number
|
||||||
|
// for infura there's a REST API Token required stored in: `REACT_APP_INFURA_TOKEN`
|
||||||
|
gasPriceOracleUrl: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EnvironmentSettings = GasPrice & {
|
||||||
|
txServiceUrl: string
|
||||||
|
// Shall we keep a reference to the relay?
|
||||||
|
relayApiUrl?: string
|
||||||
|
safeAppsUrl: string
|
||||||
|
rpcServiceUrl: string
|
||||||
|
networkExplorerName: string
|
||||||
|
networkExplorerUrl: string
|
||||||
|
networkExplorerApiUrl: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type SafeEnvironments = {
|
||||||
|
dev?: EnvironmentSettings
|
||||||
|
staging?: EnvironmentSettings
|
||||||
|
production: EnvironmentSettings
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NetworkConfig {
|
||||||
|
network: NetworkSettings
|
||||||
|
disabledFeatures?: SafeFeatures
|
||||||
|
environment: SafeEnvironments
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
import EtherLogo from 'src/assets/icons/icon_etherTokens.svg'
|
||||||
|
import { EnvironmentSettings, ETHEREUM_NETWORK, NetworkConfig } from 'src/config/networks/network.d'
|
||||||
|
|
||||||
|
const baseConfig: EnvironmentSettings = {
|
||||||
|
txServiceUrl: 'https://safe-transaction.staging.gnosisdev.com/api/v1',
|
||||||
|
safeAppsUrl: 'https://safe-apps.dev.gnosisdev.com',
|
||||||
|
gasPriceOracleUrl: 'https://ethgasstation.info/json/ethgasAPI.json',
|
||||||
|
rpcServiceUrl: 'https://rinkeby.infura.io:443/v3',
|
||||||
|
networkExplorerName: 'Etherscan',
|
||||||
|
networkExplorerUrl: 'https://rinkeby.etherscan.io',
|
||||||
|
networkExplorerApiUrl: 'https://api-rinkeby.etherscan.io/api',
|
||||||
|
}
|
||||||
|
|
||||||
|
const rinkeby: NetworkConfig = {
|
||||||
|
environment: {
|
||||||
|
dev: {
|
||||||
|
...baseConfig,
|
||||||
|
},
|
||||||
|
staging: {
|
||||||
|
...baseConfig,
|
||||||
|
safeAppsUrl: 'https://safe-apps.staging.gnosisdev.com',
|
||||||
|
},
|
||||||
|
production: {
|
||||||
|
...baseConfig,
|
||||||
|
txServiceUrl: 'https://safe-transaction.rinkeby.gnosis.io/api/v1',
|
||||||
|
safeAppsUrl: 'https://apps.gnosis-safe.io',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
network: {
|
||||||
|
id: ETHEREUM_NETWORK.RINKEBY,
|
||||||
|
backgroundColor: '#E8673C',
|
||||||
|
textColor: '#ffffff',
|
||||||
|
label: 'Rinkeby',
|
||||||
|
isTestNet: true,
|
||||||
|
nativeCoin: {
|
||||||
|
address: '0x000',
|
||||||
|
name: 'Ether',
|
||||||
|
symbol: 'ETH',
|
||||||
|
decimals: 18,
|
||||||
|
logoUri: EtherLogo,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default rinkeby
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { EnvironmentSettings, ETHEREUM_NETWORK, NetworkConfig } from 'src/config/networks/network.d'
|
||||||
|
|
||||||
|
const baseConfig: EnvironmentSettings = {
|
||||||
|
txServiceUrl: 'https://safe-transaction.xdai.gnosis.io/api/v1',
|
||||||
|
safeAppsUrl: 'https://safe-apps-xdai.staging.gnosisdev.com',
|
||||||
|
gasPrice: 1e9,
|
||||||
|
rpcServiceUrl: 'https://dai.poa.network/',
|
||||||
|
networkExplorerName: 'Blockscout',
|
||||||
|
networkExplorerUrl: 'https://blockscout.com/poa/xdai',
|
||||||
|
networkExplorerApiUrl: 'https://blockscout.com/poa/xdai/api',
|
||||||
|
}
|
||||||
|
|
||||||
|
const xDai: NetworkConfig = {
|
||||||
|
environment: {
|
||||||
|
staging: {
|
||||||
|
...baseConfig
|
||||||
|
},
|
||||||
|
production: {
|
||||||
|
...baseConfig,
|
||||||
|
safeAppsUrl: 'https://apps-xdai.gnosis-safe.io',
|
||||||
|
|
||||||
|
},
|
||||||
|
},
|
||||||
|
network: {
|
||||||
|
id: ETHEREUM_NETWORK.XDAI,
|
||||||
|
backgroundColor: '#48A8A6',
|
||||||
|
textColor: '#ffffff',
|
||||||
|
label: 'xDai',
|
||||||
|
isTestNet: false,
|
||||||
|
nativeCoin: {
|
||||||
|
address: '0x000',
|
||||||
|
name: 'xDai',
|
||||||
|
symbol: 'xDai',
|
||||||
|
decimals: 18,
|
||||||
|
logoUri: '',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default xDai
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -7,6 +7,9 @@ import { saveAddressBook } from 'src/logic/addressBook/utils'
|
||||||
import { enhanceSnackbarForAction, getNotificationsFromTxType } from 'src/logic/notifications'
|
import { enhanceSnackbarForAction, getNotificationsFromTxType } from 'src/logic/notifications'
|
||||||
import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar'
|
import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar'
|
||||||
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
|
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
|
||||||
|
import { safesListSelector } from 'src/logic/safe/store/selectors'
|
||||||
|
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
||||||
|
import updateSafe from 'src/logic/safe/store/actions/updateSafe'
|
||||||
|
|
||||||
const watchedActions = [ADD_ENTRY, REMOVE_ENTRY, UPDATE_ENTRY, ADD_OR_UPDATE_ENTRY]
|
const watchedActions = [ADD_ENTRY, REMOVE_ENTRY, UPDATE_ENTRY, ADD_OR_UPDATE_ENTRY]
|
||||||
|
|
||||||
|
@ -17,6 +20,7 @@ const addressBookMiddleware = (store) => (next) => async (action) => {
|
||||||
const state = store.getState()
|
const state = store.getState()
|
||||||
const { dispatch } = store
|
const { dispatch } = store
|
||||||
const addressBook = addressBookSelector(state)
|
const addressBook = addressBookSelector(state)
|
||||||
|
const safes = safesListSelector(state)
|
||||||
if (addressBook.length) {
|
if (addressBook.length) {
|
||||||
await saveAddressBook(addressBook)
|
await saveAddressBook(addressBook)
|
||||||
}
|
}
|
||||||
|
@ -36,8 +40,13 @@ const addressBookMiddleware = (store) => (next) => async (action) => {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case UPDATE_ENTRY: {
|
case UPDATE_ENTRY: {
|
||||||
|
const { entry } = action.payload
|
||||||
const notification = getNotificationsFromTxType(TX_NOTIFICATION_TYPES.ADDRESSBOOK_EDIT_ENTRY)
|
const notification = getNotificationsFromTxType(TX_NOTIFICATION_TYPES.ADDRESSBOOK_EDIT_ENTRY)
|
||||||
dispatch(enqueueSnackbar(enhanceSnackbarForAction(notification.afterExecution.noMoreConfirmationsNeeded)))
|
dispatch(enqueueSnackbar(enhanceSnackbarForAction(notification.afterExecution.noMoreConfirmationsNeeded)))
|
||||||
|
const safeFound = safes.find((safe) => sameAddress(safe.address, entry.address))
|
||||||
|
if (safeFound) {
|
||||||
|
dispatch(updateSafe({ address: safeFound.address, name: entry.name }))
|
||||||
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -29,9 +29,7 @@ export default handleActions(
|
||||||
|
|
||||||
const entryFound = state.find((oldEntry) => oldEntry.address === entry.address)
|
const entryFound = state.find((oldEntry) => oldEntry.address === entry.address)
|
||||||
|
|
||||||
// Only adds entries with valid names
|
if (!entryFound) {
|
||||||
const validName = getValidAddressBookName(entry.name)
|
|
||||||
if (!entryFound && validName) {
|
|
||||||
state.push(entry)
|
state.push(entry)
|
||||||
}
|
}
|
||||||
return state
|
return state
|
||||||
|
|
|
@ -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
|
|
@ -1,61 +1,11 @@
|
||||||
import { RateLimit } from 'async-sema'
|
import { RateLimit } from 'async-sema'
|
||||||
|
import { getNetworkId } from 'src/config'
|
||||||
|
|
||||||
import { ETHEREUM_NETWORK } from 'src/logic/wallets/getWeb3'
|
import { ETHEREUM_NETWORK } from 'src/config/networks/network.d'
|
||||||
|
import { Collectibles, NFTAssets, NFTTokens, OpenSeaAssets } from 'src/logic/collectibles/sources/collectibles.d'
|
||||||
import NFTIcon from 'src/routes/safe/components/Balances/assets/nft_icon.png'
|
import NFTIcon from 'src/routes/safe/components/Balances/assets/nft_icon.png'
|
||||||
import { OPENSEA_API_KEY } from 'src/utils/constants'
|
import { OPENSEA_API_KEY } from 'src/utils/constants'
|
||||||
|
|
||||||
export interface OpenSeaAssetContract {
|
|
||||||
address: string
|
|
||||||
name: string
|
|
||||||
image_url: string
|
|
||||||
symbol: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface OpenSeaCollection {
|
|
||||||
name: string
|
|
||||||
slug: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface OpenSeaAsset {
|
|
||||||
asset_contract: OpenSeaAssetContract
|
|
||||||
background_color: string
|
|
||||||
collection: OpenSeaCollection
|
|
||||||
description: string
|
|
||||||
image_thumbnail_url: string
|
|
||||||
name: string
|
|
||||||
token_id: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type OpenSeaAssets = Array<OpenSeaAsset>
|
|
||||||
|
|
||||||
export interface NFTAsset {
|
|
||||||
address: string
|
|
||||||
assetContract: OpenSeaAssetContract
|
|
||||||
collection: OpenSeaCollection
|
|
||||||
description: string
|
|
||||||
image: string
|
|
||||||
name: string
|
|
||||||
numberOfTokens: number
|
|
||||||
slug: string
|
|
||||||
symbol: string
|
|
||||||
}
|
|
||||||
export type NFTAssets = Record<string, NFTAsset>
|
|
||||||
|
|
||||||
export interface NFTToken {
|
|
||||||
assetAddress: string
|
|
||||||
color: string
|
|
||||||
description: string
|
|
||||||
image: string
|
|
||||||
name: string
|
|
||||||
tokenId: number | string
|
|
||||||
}
|
|
||||||
export type NFTTokens = Array<NFTToken>
|
|
||||||
|
|
||||||
export interface Collectibles {
|
|
||||||
nftAssets: NFTAssets
|
|
||||||
nftTokens: NFTTokens
|
|
||||||
}
|
|
||||||
|
|
||||||
class OpenSea {
|
class OpenSea {
|
||||||
_rateLimit = async (): Promise<void> => {}
|
_rateLimit = async (): Promise<void> => {}
|
||||||
|
|
||||||
|
@ -133,14 +83,11 @@ class OpenSea {
|
||||||
* Fetches from OpenSea the list of collectibles, grouped by category,
|
* Fetches from OpenSea the list of collectibles, grouped by category,
|
||||||
* for the provided Safe Address in the specified Network
|
* for the provided Safe Address in the specified Network
|
||||||
* @param {string} safeAddress
|
* @param {string} safeAddress
|
||||||
* @param {string} network
|
|
||||||
* @returns {Promise<Collectibles>}
|
* @returns {Promise<Collectibles>}
|
||||||
*/
|
*/
|
||||||
async fetchAllUserCollectiblesByCategoryAsync(safeAddress: string, network: string): Promise<Collectibles> {
|
async fetchCollectibles(safeAddress: string): Promise<Collectibles> {
|
||||||
// eslint-disable-next-line no-underscore-dangle
|
const metadataSourceUrl = this._endpointsUrls[getNetworkId()]
|
||||||
const metadataSourceUrl = this._endpointsUrls[network]
|
|
||||||
const url = `${metadataSourceUrl}/assets/?owner=${safeAddress}`
|
const url = `${metadataSourceUrl}/assets/?owner=${safeAddress}`
|
||||||
// eslint-disable-next-line no-underscore-dangle
|
|
||||||
const assetsResponse = await this._fetch(url)
|
const assetsResponse = await this._fetch(url)
|
||||||
const assetsResponseJson = await assetsResponse.json()
|
const assetsResponseJson = await assetsResponse.json()
|
||||||
return OpenSea.extractCollectiblesInfo(assetsResponseJson)
|
return OpenSea.extractCollectiblesInfo(assetsResponseJson)
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -1,13 +1,15 @@
|
||||||
import MockedOpenSea from 'src/logic/collectibles/sources/MockedOpenSea'
|
import MockedOpenSea from 'src/logic/collectibles/sources/MockedOpenSea'
|
||||||
import OpenSea from 'src/logic/collectibles/sources/OpenSea'
|
import OpenSea from 'src/logic/collectibles/sources/OpenSea'
|
||||||
|
import Gnosis from 'src/logic/collectibles/sources/Gnosis'
|
||||||
import { COLLECTIBLES_SOURCE } from 'src/utils/constants'
|
import { COLLECTIBLES_SOURCE } from 'src/utils/constants'
|
||||||
|
|
||||||
const SOURCES = {
|
const SOURCES = {
|
||||||
opensea: new OpenSea({ rps: 4 }),
|
opensea: new OpenSea({ rps: 4 }),
|
||||||
|
gnosis: new Gnosis({ rps: 4 }),
|
||||||
mockedopensea: new MockedOpenSea({ rps: 4 }),
|
mockedopensea: new MockedOpenSea({ rps: 4 }),
|
||||||
}
|
}
|
||||||
|
|
||||||
type Sources = typeof SOURCES
|
type Sources = typeof SOURCES
|
||||||
|
|
||||||
export const getConfiguredSource = (): Sources['opensea'] | Sources['mockedopensea'] =>
|
export const getConfiguredSource = (): Sources['opensea'] | Sources['mockedopensea'] | Sources['gnosis'] =>
|
||||||
SOURCES[COLLECTIBLES_SOURCE.toLowerCase()]
|
SOURCES[COLLECTIBLES_SOURCE.toLowerCase()]
|
||||||
|
|
|
@ -1,15 +1,13 @@
|
||||||
import { batch } from 'react-redux'
|
import { batch } from 'react-redux'
|
||||||
|
import { Dispatch } from 'redux'
|
||||||
|
|
||||||
import { getNetwork } from 'src/config'
|
|
||||||
import { getConfiguredSource } from 'src/logic/collectibles/sources'
|
import { getConfiguredSource } from 'src/logic/collectibles/sources'
|
||||||
import { addNftAssets, addNftTokens } from 'src/logic/collectibles/store/actions/addCollectibles'
|
import { addNftAssets, addNftTokens } from 'src/logic/collectibles/store/actions/addCollectibles'
|
||||||
import { Dispatch } from 'redux'
|
|
||||||
|
|
||||||
const fetchCollectibles = (safeAddress: string) => async (dispatch: Dispatch): Promise<void> => {
|
const fetchCollectibles = (safeAddress: string) => async (dispatch: Dispatch): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const network = getNetwork()
|
|
||||||
const source = getConfiguredSource()
|
const source = getConfiguredSource()
|
||||||
const collectibles = await source.fetchAllUserCollectiblesByCategoryAsync(safeAddress, network)
|
const collectibles = await source.fetchCollectibles(safeAddress)
|
||||||
|
|
||||||
batch(() => {
|
batch(() => {
|
||||||
dispatch(addNftAssets(collectibles.nftAssets))
|
dispatch(addNftAssets(collectibles.nftAssets))
|
||||||
|
|
|
@ -1,14 +1,18 @@
|
||||||
import { createSelector } from 'reselect'
|
import { createSelector } from 'reselect'
|
||||||
import { NFTAsset, NFTAssets, NFTTokens } from 'src/logic/collectibles/sources/OpenSea'
|
import { NFTAsset, NFTAssets, NFTTokens } from 'src/logic/collectibles/sources/collectibles.d'
|
||||||
|
|
||||||
import { AppReduxState } from 'src/store'
|
import { AppReduxState } from 'src/store'
|
||||||
import { NFT_ASSETS_REDUCER_ID, NFT_TOKENS_REDUCER_ID } from 'src/logic/collectibles/store/reducer/collectibles'
|
import { NFT_ASSETS_REDUCER_ID, NFT_TOKENS_REDUCER_ID } from 'src/logic/collectibles/store/reducer/collectibles'
|
||||||
import { safeActiveAssetsSelector } from 'src/logic/safe/store/selectors'
|
import { safeActiveAssetsSelector } from 'src/logic/safe/store/selectors'
|
||||||
|
|
||||||
export const nftAssetsSelector = (state: AppReduxState): NFTAssets => state[NFT_ASSETS_REDUCER_ID]
|
export const nftAssets = (state: AppReduxState): NFTAssets => state[NFT_ASSETS_REDUCER_ID]
|
||||||
export const nftTokensSelector = (state: AppReduxState): NFTTokens => state[NFT_TOKENS_REDUCER_ID]
|
export const nftTokens = (state: AppReduxState): NFTTokens => state[NFT_TOKENS_REDUCER_ID]
|
||||||
|
|
||||||
export const nftAssetsListSelector = createSelector(nftAssetsSelector, (assets): NFTAsset[] => {
|
export const nftAssetsSelector = createSelector(nftAssets, (assets) => assets)
|
||||||
|
|
||||||
|
export const nftTokensSelector = createSelector(nftTokens, (tokens) => tokens)
|
||||||
|
|
||||||
|
export const nftAssetsListSelector = createSelector(nftAssets, (assets): NFTAsset[] => {
|
||||||
return assets ? Object.values(assets) : []
|
return assets ? Object.values(assets) : []
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
|
@ -1,7 +0,0 @@
|
||||||
import EtherscanService from 'src/logic/contractInteraction/sources/EtherscanService'
|
|
||||||
|
|
||||||
const sources = {
|
|
||||||
etherscan: new EtherscanService({ rps: 4 }),
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getConfiguredSource = () => sources['etherscan']
|
|
|
@ -1,4 +1,6 @@
|
||||||
import { web3ReadOnly as web3 } from 'src/logic/wallets/getWeb3'
|
import { web3ReadOnly as web3 } from 'src/logic/wallets/getWeb3'
|
||||||
|
import { BatchRequest } from 'web3-core'
|
||||||
|
import { AbiItem } from 'web3-utils'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a batch request for grouping RPC calls
|
* Generates a batch request for grouping RPC calls
|
||||||
|
@ -10,23 +12,33 @@ import { web3ReadOnly as web3 } from 'src/logic/wallets/getWeb3'
|
||||||
* @param {array<{ args: [any], method: string, type: 'eth'|undefined } | string>} args.methods - methods to be called
|
* @param {array<{ args: [any], method: string, type: 'eth'|undefined } | string>} args.methods - methods to be called
|
||||||
* @returns {Promise<[*]>}
|
* @returns {Promise<[*]>}
|
||||||
*/
|
*/
|
||||||
const generateBatchRequests = ({ abi, address, batch, context, methods }: any): any => {
|
type MethodsArgsType = Array<string | number>
|
||||||
const contractInstance: any = new web3.eth.Contract(abi, address)
|
|
||||||
|
interface Props {
|
||||||
|
abi: AbiItem[]
|
||||||
|
address: string
|
||||||
|
batch?: BatchRequest
|
||||||
|
context?: unknown
|
||||||
|
methods: Array<string | {method: string, type?: string, args: MethodsArgsType }>
|
||||||
|
}
|
||||||
|
|
||||||
|
const generateBatchRequests = <ReturnValues>({ abi, address, batch, context, methods }: Props): Promise<ReturnValues> => {
|
||||||
|
const contractInstance = new web3.eth.Contract(abi, address)
|
||||||
const localBatch = new web3.BatchRequest()
|
const localBatch = new web3.BatchRequest()
|
||||||
|
|
||||||
const values = methods.map((methodObject) => {
|
const values = methods.map((methodObject) => {
|
||||||
let method, type, args = []
|
let method, type, args: MethodsArgsType = []
|
||||||
|
|
||||||
if (typeof methodObject === 'string') {
|
if (typeof methodObject === 'string') {
|
||||||
method = methodObject
|
method = methodObject
|
||||||
} else {
|
} else {
|
||||||
;({ method, type, args = [] } = methodObject)
|
({ method, type, args } = methodObject)
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const resolver = (error, result) => {
|
const resolver = (error, result) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
resolve(null)
|
resolve()
|
||||||
} else {
|
} else {
|
||||||
resolve(result)
|
resolve(result)
|
||||||
}
|
}
|
||||||
|
@ -43,7 +55,8 @@ const generateBatchRequests = ({ abi, address, batch, context, methods }: any):
|
||||||
// If batch was provided add to external batch
|
// If batch was provided add to external batch
|
||||||
batch ? batch.add(request) : localBatch.add(request)
|
batch ? batch.add(request) : localBatch.add(request)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
resolve(null)
|
console.error('There was an error trying to batch request from web3.', e)
|
||||||
|
resolve()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -54,9 +67,8 @@ const generateBatchRequests = ({ abi, address, batch, context, methods }: any):
|
||||||
// in the outside function where the batch object is created.
|
// in the outside function where the batch object is created.
|
||||||
!batch && localBatch.execute()
|
!batch && localBatch.execute()
|
||||||
|
|
||||||
const returnValues = context ? [context, ...values] : values
|
// @ts-ignore
|
||||||
|
return Promise.all([context, ...values])
|
||||||
return Promise.all(returnValues)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default generateBatchRequests
|
export default generateBatchRequests
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
import { AbiItem } from 'web3-utils'
|
import { AbiItem } from 'web3-utils'
|
||||||
import contract from 'truffle-contract'
|
|
||||||
import Web3 from 'web3'
|
|
||||||
import ProxyFactorySol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafeProxyFactory.json'
|
|
||||||
import GnosisSafeSol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafe.json'
|
import GnosisSafeSol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafe.json'
|
||||||
import SafeProxy from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafeProxy.json'
|
|
||||||
import { ensureOnce } from 'src/utils/singleton'
|
|
||||||
import memoize from 'lodash.memoize'
|
import memoize from 'lodash.memoize'
|
||||||
import { getWeb3, getNetworkIdFrom } from 'src/logic/wallets/getWeb3'
|
import ProxyFactorySol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafeProxyFactory.json'
|
||||||
import { calculateGasOf, calculateGasPrice } from 'src/logic/wallets/ethTransactions'
|
import SafeProxy from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafeProxy.json'
|
||||||
import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses'
|
import Web3 from 'web3'
|
||||||
|
|
||||||
|
import { ETHEREUM_NETWORK } from 'src/config/networks/network.d'
|
||||||
import { isProxyCode } from 'src/logic/contracts/historicProxyCode'
|
import { isProxyCode } from 'src/logic/contracts/historicProxyCode'
|
||||||
import { GnosisSafeProxyFactory } from 'src/types/contracts/GnosisSafeProxyFactory.d';
|
import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses'
|
||||||
|
import { calculateGasOf, calculateGasPrice } from 'src/logic/wallets/ethTransactions'
|
||||||
|
import { getWeb3, getNetworkIdFrom } from 'src/logic/wallets/getWeb3'
|
||||||
import { GnosisSafe } from 'src/types/contracts/GnosisSafe.d'
|
import { GnosisSafe } from 'src/types/contracts/GnosisSafe.d'
|
||||||
|
import { GnosisSafeProxyFactory } from 'src/types/contracts/GnosisSafeProxyFactory.d'
|
||||||
|
|
||||||
export const SENTINEL_ADDRESS = '0x0000000000000000000000000000000000000001'
|
export const SENTINEL_ADDRESS = '0x0000000000000000000000000000000000000001'
|
||||||
export const MULTI_SEND_ADDRESS = '0x8d29be29923b68abfdd21e541b9374737b49cdad'
|
export const MULTI_SEND_ADDRESS = '0x8d29be29923b68abfdd21e541b9374737b49cdad'
|
||||||
|
@ -20,24 +20,39 @@ export const DEFAULT_FALLBACK_HANDLER_ADDRESS = '0xd5D82B6aDDc9027B22dCA772Aa68D
|
||||||
export const SAFE_MASTER_COPY_ADDRESS_V10 = '0xb6029EA3B2c51D09a50B53CA8012FeEB05bDa35A'
|
export const SAFE_MASTER_COPY_ADDRESS_V10 = '0xb6029EA3B2c51D09a50B53CA8012FeEB05bDa35A'
|
||||||
|
|
||||||
|
|
||||||
let proxyFactoryMaster
|
let proxyFactoryMaster: GnosisSafeProxyFactory
|
||||||
let safeMaster
|
let safeMaster: GnosisSafe
|
||||||
|
|
||||||
const createGnosisSafeContract = (web3: Web3) => {
|
/**
|
||||||
const gnosisSafe = contract(GnosisSafeSol)
|
* Creates a Contract instance of the GnosisSafe contract
|
||||||
gnosisSafe.setProvider(web3.currentProvider)
|
* @param {Web3} web3
|
||||||
|
* @param {ETHEREUM_NETWORK} networkId
|
||||||
return gnosisSafe
|
*/
|
||||||
|
const createGnosisSafeContract = (web3: Web3, networkId: ETHEREUM_NETWORK) => {
|
||||||
|
const networks = GnosisSafeSol.networks
|
||||||
|
// TODO: this may not be the most scalable approach,
|
||||||
|
// but up until v1.2.0 the address is the same for all the networks.
|
||||||
|
// So, if we can't find the network in the Contract artifact, we fallback to MAINNET.
|
||||||
|
const contractAddress = networks[networkId]?.address ?? networks[ETHEREUM_NETWORK.MAINNET].address
|
||||||
|
return new web3.eth.Contract(GnosisSafeSol.abi as AbiItem[], contractAddress) as unknown as GnosisSafe
|
||||||
}
|
}
|
||||||
|
|
||||||
const createProxyFactoryContract = (web3: Web3, networkId: number): GnosisSafeProxyFactory => {
|
/**
|
||||||
const contractAddress = ProxyFactorySol.networks[networkId].address
|
* Creates a Contract instance of the GnosisSafeProxyFactory contract
|
||||||
const proxyFactory = new web3.eth.Contract(ProxyFactorySol.abi as AbiItem[], contractAddress) as unknown as GnosisSafeProxyFactory
|
* @param {Web3} web3
|
||||||
|
* @param {ETHEREUM_NETWORK} networkId
|
||||||
return proxyFactory
|
*/
|
||||||
|
const createProxyFactoryContract = (web3: Web3, networkId: ETHEREUM_NETWORK): GnosisSafeProxyFactory => {
|
||||||
|
const networks = ProxyFactorySol.networks
|
||||||
|
// TODO: this may not be the most scalable approach,
|
||||||
|
// but up until v1.2.0 the address is the same for all the networks.
|
||||||
|
// So, if we can't find the network in the Contract artifact, we fallback to MAINNET.
|
||||||
|
const contractAddress = networks[networkId]?.address ?? networks[ETHEREUM_NETWORK.MAINNET].address
|
||||||
|
return new web3.eth.Contract(ProxyFactorySol.abi as AbiItem[], contractAddress) as unknown as GnosisSafeProxyFactory
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getGnosisSafeContract = memoize(createGnosisSafeContract)
|
export const getGnosisSafeContract = memoize(createGnosisSafeContract)
|
||||||
|
|
||||||
const getCreateProxyFactoryContract = memoize(createProxyFactoryContract)
|
const getCreateProxyFactoryContract = memoize(createProxyFactoryContract)
|
||||||
|
|
||||||
const instantiateMasterCopies = async () => {
|
const instantiateMasterCopies = async () => {
|
||||||
|
@ -47,25 +62,11 @@ const instantiateMasterCopies = async () => {
|
||||||
// Create ProxyFactory Master Copy
|
// Create ProxyFactory Master Copy
|
||||||
proxyFactoryMaster = getCreateProxyFactoryContract(web3, networkId)
|
proxyFactoryMaster = getCreateProxyFactoryContract(web3, networkId)
|
||||||
|
|
||||||
// Initialize Safe master copy
|
// Create Safe Master copy
|
||||||
const GnosisSafe = getGnosisSafeContract(web3)
|
safeMaster = getGnosisSafeContract(web3, networkId)
|
||||||
safeMaster = await GnosisSafe.deployed()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ONLY USED IN TEST ENVIRONMENT
|
export const initContracts = instantiateMasterCopies
|
||||||
const createMasterCopies = async () => {
|
|
||||||
const web3 = getWeb3()
|
|
||||||
const accounts = await web3.eth.getAccounts()
|
|
||||||
const userAccount = accounts[0]
|
|
||||||
|
|
||||||
const ProxyFactory = getCreateProxyFactoryContract(web3, 4441)
|
|
||||||
proxyFactoryMaster = await ProxyFactory.deploy({ data: GnosisSafeSol.bytecode }).send({ from: userAccount, gas: 5000000 })
|
|
||||||
|
|
||||||
const GnosisSafe = getGnosisSafeContract(web3)
|
|
||||||
safeMaster = await GnosisSafe.new({ from: userAccount, gas: '7000000' })
|
|
||||||
}
|
|
||||||
|
|
||||||
export const initContracts = process.env.NODE_ENV === 'test' ? ensureOnce(createMasterCopies) : instantiateMasterCopies
|
|
||||||
|
|
||||||
export const getSafeMasterContract = async () => {
|
export const getSafeMasterContract = async () => {
|
||||||
await initContracts()
|
await initContracts()
|
||||||
|
@ -74,11 +75,11 @@ export const getSafeMasterContract = async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getSafeDeploymentTransaction = (safeAccounts, numConfirmations) => {
|
export const getSafeDeploymentTransaction = (safeAccounts, numConfirmations) => {
|
||||||
const gnosisSafeData = safeMaster.contract.methods
|
const gnosisSafeData = safeMaster.methods
|
||||||
.setup(safeAccounts, numConfirmations, ZERO_ADDRESS, '0x', DEFAULT_FALLBACK_HANDLER_ADDRESS, ZERO_ADDRESS, 0, ZERO_ADDRESS)
|
.setup(safeAccounts, numConfirmations, ZERO_ADDRESS, '0x', DEFAULT_FALLBACK_HANDLER_ADDRESS, ZERO_ADDRESS, 0, ZERO_ADDRESS)
|
||||||
.encodeABI()
|
.encodeABI()
|
||||||
|
|
||||||
return proxyFactoryMaster.methods.createProxy(safeMaster.address, gnosisSafeData)
|
return proxyFactoryMaster.methods.createProxy(safeMaster.options.address, gnosisSafeData)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const estimateGasForDeployingSafe = async (
|
export const estimateGasForDeployingSafe = async (
|
||||||
|
@ -86,13 +87,13 @@ export const estimateGasForDeployingSafe = async (
|
||||||
numConfirmations,
|
numConfirmations,
|
||||||
userAccount,
|
userAccount,
|
||||||
) => {
|
) => {
|
||||||
const gnosisSafeData = await safeMaster.contract.methods
|
const gnosisSafeData = await safeMaster.methods
|
||||||
.setup(safeAccounts, numConfirmations, ZERO_ADDRESS, '0x', DEFAULT_FALLBACK_HANDLER_ADDRESS, ZERO_ADDRESS, 0, ZERO_ADDRESS)
|
.setup(safeAccounts, numConfirmations, ZERO_ADDRESS, '0x', DEFAULT_FALLBACK_HANDLER_ADDRESS, ZERO_ADDRESS, 0, ZERO_ADDRESS)
|
||||||
.encodeABI()
|
.encodeABI()
|
||||||
const proxyFactoryData = proxyFactoryMaster.methods
|
const proxyFactoryData = proxyFactoryMaster.methods
|
||||||
.createProxy(safeMaster.address, gnosisSafeData)
|
.createProxy(safeMaster.options.address, gnosisSafeData)
|
||||||
.encodeABI()
|
.encodeABI()
|
||||||
const gas = await calculateGasOf(proxyFactoryData, userAccount, proxyFactoryMaster.address)
|
const gas = await calculateGasOf(proxyFactoryData, userAccount, proxyFactoryMaster.options.address)
|
||||||
const gasPrice = await calculateGasPrice()
|
const gasPrice = await calculateGasPrice()
|
||||||
|
|
||||||
return gas * parseInt(gasPrice, 10)
|
return gas * parseInt(gasPrice, 10)
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import Cookies from 'js-cookie'
|
import Cookies from 'js-cookie'
|
||||||
|
|
||||||
import { getNetwork } from 'src/config'
|
import { getNetworkName } from 'src/config'
|
||||||
|
|
||||||
const PREFIX = `v1_${getNetwork()}`
|
const PREFIX = `v1_${getNetworkName()}`
|
||||||
|
|
||||||
export const loadFromCookie = async (key) => {
|
export const loadFromCookie = async (key) => {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { aNewStore } from 'src/store'
|
import { aNewStore } from 'src/store'
|
||||||
import fetchTokenCurrenciesBalances from 'src/logic/currencyValues/api/fetchTokenCurrenciesBalances'
|
import { fetchTokenCurrenciesBalances } from 'src/logic/currencyValues/api/fetchTokenCurrenciesBalances'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { getTxServiceHost } from 'src/config'
|
import { getTxServiceUrl } from 'src/config'
|
||||||
|
|
||||||
jest.mock('axios')
|
jest.mock('axios')
|
||||||
describe('fetchTokenCurrenciesBalances', () => {
|
describe('fetchTokenCurrenciesBalances', () => {
|
||||||
|
@ -19,26 +19,28 @@ describe('fetchTokenCurrenciesBalances', () => {
|
||||||
// given
|
// given
|
||||||
const expectedResult = [
|
const expectedResult = [
|
||||||
{
|
{
|
||||||
balance: '849890000000000000',
|
|
||||||
balanceUsd: '337.2449',
|
|
||||||
token: null,
|
|
||||||
tokenAddress: null,
|
tokenAddress: null,
|
||||||
usdConversion: '396.81',
|
token: null,
|
||||||
|
balance: '849890000000000000',
|
||||||
|
fiatBalance: '337.2449',
|
||||||
|
fiatConversion: '396.81',
|
||||||
|
fiatCode: 'USD',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
balance: '24698677800000000000',
|
tokenAddress: '0x5592EC0cfb4dbc12D3aB100b257153436a1f0FEa',
|
||||||
balanceUsd: '29.3432',
|
|
||||||
token: {
|
token: {
|
||||||
name: 'Dai',
|
name: 'Dai',
|
||||||
symbol: 'DAI',
|
symbol: 'DAI',
|
||||||
decimals: 18,
|
decimals: 18,
|
||||||
logoUri: 'https://gnosis-safe-token-logos.s3.amazonaws.com/0x5592EC0cfb4dbc12D3aB100b257153436a1f0FEa.png',
|
logoUri: 'https://gnosis-safe-token-logos.s3.amazonaws.com/0x5592EC0cfb4dbc12D3aB100b257153436a1f0FEa.png',
|
||||||
},
|
},
|
||||||
tokenAddress: '0x5592EC0cfb4dbc12D3aB100b257153436a1f0FEa',
|
balance: '24698677800000000000',
|
||||||
usdConversion: '1.188',
|
fiatBalance: '29.3432',
|
||||||
|
fiatConversion: '1.188',
|
||||||
|
fiatCode: 'USD',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
const apiUrl = getTxServiceHost()
|
const apiUrl = getTxServiceUrl()
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
axios.get.mockImplementationOnce(() => Promise.resolve(expectedResult))
|
axios.get.mockImplementationOnce(() => Promise.resolve(expectedResult))
|
||||||
|
@ -49,8 +51,6 @@ describe('fetchTokenCurrenciesBalances', () => {
|
||||||
// then
|
// then
|
||||||
expect(result).toStrictEqual(expectedResult)
|
expect(result).toStrictEqual(expectedResult)
|
||||||
expect(axios.get).toHaveBeenCalled()
|
expect(axios.get).toHaveBeenCalled()
|
||||||
expect(axios.get).toBeCalledWith(`${apiUrl}safes/${safeAddress}/balances/usd/?exclude_spam=${excludeSpamTokens}`, {
|
expect(axios.get).toBeCalledWith(`${apiUrl}/safes/${safeAddress}/balances/usd/?exclude_spam=${excludeSpamTokens}`)
|
||||||
params: { limit: 3000 },
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
import { getExchangeRatesUrl } from 'src/config'
|
import { EXCHANGE_RATE_URL } from 'src/utils/constants'
|
||||||
import { AVAILABLE_CURRENCIES } from '../store/model/currencyValues'
|
import { AVAILABLE_CURRENCIES } from '../store/model/currencyValues'
|
||||||
import fetchTokenCurrenciesBalances from './fetchTokenCurrenciesBalances'
|
import { fetchTokenCurrenciesBalances } from './fetchTokenCurrenciesBalances'
|
||||||
import BigNumber from 'bignumber.js'
|
import BigNumber from 'bignumber.js'
|
||||||
|
|
||||||
const fetchCurrenciesRates = async (
|
const fetchCurrenciesRates = async (
|
||||||
|
@ -16,7 +16,7 @@ const fetchCurrenciesRates = async (
|
||||||
try {
|
try {
|
||||||
const result = await fetchTokenCurrenciesBalances(safeAddress)
|
const result = await fetchTokenCurrenciesBalances(safeAddress)
|
||||||
if (result?.data?.length) {
|
if (result?.data?.length) {
|
||||||
rate = new BigNumber(1).div(result.data[0].usdConversion).toNumber()
|
rate = new BigNumber(1).div(result.data[0].fiatConversion).toNumber()
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fetching ETH data from the relayer errored', error)
|
console.error('Fetching ETH data from the relayer errored', error)
|
||||||
|
@ -25,7 +25,7 @@ const fetchCurrenciesRates = async (
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const url = `${getExchangeRatesUrl()}?base=${baseCurrency}&symbols=${targetCurrencyValue}`
|
const url = `${EXCHANGE_RATE_URL}?base=${baseCurrency}&symbols=${targetCurrencyValue}`
|
||||||
const result = await axios.get(url)
|
const result = await axios.get(url)
|
||||||
if (result?.data) {
|
if (result?.data) {
|
||||||
const { rates } = result.data
|
const { rates } = result.data
|
||||||
|
|
|
@ -1,28 +1,24 @@
|
||||||
import axios, { AxiosResponse } from 'axios'
|
import axios, { AxiosResponse } from 'axios'
|
||||||
|
|
||||||
import { getTxServiceHost } from 'src/config'
|
import { getTxServiceUrl } from 'src/config'
|
||||||
import { TokenProps } from 'src/logic/tokens/store/model/token'
|
import { TokenProps } from 'src/logic/tokens/store/model/token'
|
||||||
|
import { AVAILABLE_CURRENCIES } from '../store/model/currencyValues'
|
||||||
|
|
||||||
export type BalanceEndpoint = {
|
export type BalanceEndpoint = {
|
||||||
balance: string
|
|
||||||
balanceUsd: string
|
|
||||||
tokenAddress: string
|
tokenAddress: string
|
||||||
token?: TokenProps
|
token?: TokenProps
|
||||||
usdConversion: string
|
balance: string
|
||||||
|
fiatBalance: string
|
||||||
|
fiatConversion: string
|
||||||
|
fiatCode: AVAILABLE_CURRENCIES
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchTokenCurrenciesBalances = (
|
export const fetchTokenCurrenciesBalances = (
|
||||||
safeAddress: string,
|
safeAddress: string,
|
||||||
excludeSpamTokens = true,
|
excludeSpamTokens = true,
|
||||||
): Promise<AxiosResponse<BalanceEndpoint[]>> => {
|
): Promise<AxiosResponse<BalanceEndpoint[]>> => {
|
||||||
const apiUrl = getTxServiceHost()
|
const apiUrl = getTxServiceUrl()
|
||||||
const url = `${apiUrl}safes/${safeAddress}/balances/usd/?exclude_spam=${excludeSpamTokens}`
|
const url = `${apiUrl}/safes/${safeAddress}/balances/usd/?exclude_spam=${excludeSpamTokens}`
|
||||||
|
|
||||||
return axios.get(url, {
|
return axios.get(url)
|
||||||
params: {
|
|
||||||
limit: 3000,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default fetchTokenCurrenciesBalances
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { OptionsObject } from 'notistack'
|
import { OptionsObject } from 'notistack'
|
||||||
|
|
||||||
import { getNetwork } from 'src/config'
|
import { getNetworkName } from 'src/config'
|
||||||
import { capitalize } from 'src/utils/css'
|
|
||||||
|
|
||||||
export const SUCCESS = 'success'
|
export const SUCCESS = 'success'
|
||||||
export const ERROR = 'error'
|
export const ERROR = 'error'
|
||||||
|
@ -46,7 +45,7 @@ const NOTIFICATION_IDS = {
|
||||||
SETTINGS_CHANGE_EXECUTED_MSG: 'SETTINGS_CHANGE_EXECUTED_MSG',
|
SETTINGS_CHANGE_EXECUTED_MSG: 'SETTINGS_CHANGE_EXECUTED_MSG',
|
||||||
SETTINGS_CHANGE_EXECUTED_MORE_CONFIRMATIONS_MSG: 'SETTINGS_CHANGE_EXECUTED_MORE_CONFIRMATIONS_MSG',
|
SETTINGS_CHANGE_EXECUTED_MORE_CONFIRMATIONS_MSG: 'SETTINGS_CHANGE_EXECUTED_MORE_CONFIRMATIONS_MSG',
|
||||||
SETTINGS_CHANGE_FAILED_MSG: 'SETTINGS_CHANGE_FAILED_MSG',
|
SETTINGS_CHANGE_FAILED_MSG: 'SETTINGS_CHANGE_FAILED_MSG',
|
||||||
RINKEBY_VERSION_MSG: 'RINKEBY_VERSION_MSG',
|
TESTNET_VERSION_MSG: 'TESTNET_VERSION_MSG',
|
||||||
WRONG_NETWORK_MSG: 'WRONG_NETWORK_MSG',
|
WRONG_NETWORK_MSG: 'WRONG_NETWORK_MSG',
|
||||||
ADDRESS_BOOK_NEW_ENTRY_SUCCESS: 'ADDRESS_BOOK_NEW_ENTRY_SUCCESS',
|
ADDRESS_BOOK_NEW_ENTRY_SUCCESS: 'ADDRESS_BOOK_NEW_ENTRY_SUCCESS',
|
||||||
ADDRESS_BOOK_EDIT_ENTRY_SUCCESS: 'ADDRESS_BOOK_EDIT_ENTRY_SUCCESS',
|
ADDRESS_BOOK_EDIT_ENTRY_SUCCESS: 'ADDRESS_BOOK_EDIT_ENTRY_SUCCESS',
|
||||||
|
@ -193,12 +192,12 @@ export const NOTIFICATIONS: Record<NotificationId, Notification> = {
|
||||||
},
|
},
|
||||||
|
|
||||||
// Network
|
// Network
|
||||||
RINKEBY_VERSION_MSG: {
|
TESTNET_VERSION_MSG: {
|
||||||
message: "Rinkeby Version: Don't send Mainnet assets to this Safe",
|
message: "Testnet Version: Don't send production assets to this Safe",
|
||||||
options: { variant: WARNING, persist: true, preventDuplicate: true },
|
options: { variant: WARNING, persist: true, preventDuplicate: true },
|
||||||
},
|
},
|
||||||
WRONG_NETWORK_MSG: {
|
WRONG_NETWORK_MSG: {
|
||||||
message: `Wrong network: Please use ${capitalize(getNetwork())}`,
|
message: `Wrong network: Please use ${getNetworkName()}`,
|
||||||
options: { variant: WARNING, persist: true, preventDuplicate: true },
|
options: { variant: WARNING, persist: true, preventDuplicate: true },
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,14 @@
|
||||||
import { useState, useMemo } from 'react'
|
import { useState, useMemo } from 'react'
|
||||||
|
|
||||||
const INITIAL_STATE = {
|
type SafeActionsState = {
|
||||||
|
sendFunds: {
|
||||||
|
isOpen: boolean
|
||||||
|
selectedToken?: string
|
||||||
|
}
|
||||||
|
showReceive: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const INITIAL_STATE: SafeActionsState = {
|
||||||
sendFunds: {
|
sendFunds: {
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
selectedToken: undefined,
|
selectedToken: undefined,
|
||||||
|
@ -13,7 +21,7 @@ type Response = {
|
||||||
onHide: (action: string) => void
|
onHide: (action: string) => void
|
||||||
showSendFunds: (token: string) => void
|
showSendFunds: (token: string) => void
|
||||||
hideSendFunds: () => void
|
hideSendFunds: () => void
|
||||||
safeActionsState: Record<string, unknown>
|
safeActionsState: SafeActionsState
|
||||||
}
|
}
|
||||||
|
|
||||||
const useSafeActions = (): Response => {
|
const useSafeActions = (): Response => {
|
||||||
|
|
|
@ -572,18 +572,6 @@ describe('calculateTransactionStatus', () => {
|
||||||
// then
|
// then
|
||||||
expect(result).toBe(TransactionStatus.PENDING)
|
expect(result).toBe(TransactionStatus.PENDING)
|
||||||
})
|
})
|
||||||
it('It should return PENDING if the tx has no confirmations', () => {
|
|
||||||
// given
|
|
||||||
const transaction = makeTransaction({ confirmations: List(), isPending: false })
|
|
||||||
const safe = makeSafe({ threshold: 3 })
|
|
||||||
const currentUser = safeAddress
|
|
||||||
|
|
||||||
// when
|
|
||||||
const result = calculateTransactionStatus(transaction, safe, currentUser)
|
|
||||||
|
|
||||||
// then
|
|
||||||
expect(result).toBe(TransactionStatus.PENDING)
|
|
||||||
})
|
|
||||||
it('It should return AWAITING_CONFIRMATIONS if the tx has confirmations bellow the threshold, the user is owner and signed', () => {
|
it('It should return AWAITING_CONFIRMATIONS if the tx has confirmations bellow the threshold, the user is owner and signed', () => {
|
||||||
// given
|
// given
|
||||||
const userAddress = 'address1'
|
const userAddress = 'address1'
|
||||||
|
@ -762,7 +750,36 @@ describe('calculateTransactionType', () => {
|
||||||
describe('buildTx', () => {
|
describe('buildTx', () => {
|
||||||
it('Returns a valid transaction', async () => {
|
it('Returns a valid transaction', async () => {
|
||||||
// given
|
// given
|
||||||
const cancelTx1 = makeTransaction()
|
const cancelTx1 = {
|
||||||
|
baseGas: 0,
|
||||||
|
blockNumber: 0,
|
||||||
|
confirmations: [],
|
||||||
|
confirmationsRequired: 2,
|
||||||
|
data: null,
|
||||||
|
dataDecoded: undefined,
|
||||||
|
ethGasPrice: '0',
|
||||||
|
executionDate: null,
|
||||||
|
executor: '',
|
||||||
|
fee: '',
|
||||||
|
gasPrice: '',
|
||||||
|
gasToken: '',
|
||||||
|
gasUsed: 0,
|
||||||
|
isExecuted: false,
|
||||||
|
isSuccessful: true,
|
||||||
|
modified: '',
|
||||||
|
nonce: 0,
|
||||||
|
operation: 0,
|
||||||
|
origin: null,
|
||||||
|
refundReceiver: '',
|
||||||
|
safe: '',
|
||||||
|
safeTxGas: 0,
|
||||||
|
safeTxHash: '',
|
||||||
|
signatures: '',
|
||||||
|
submissionDate: null,
|
||||||
|
to: '',
|
||||||
|
transactionHash: null,
|
||||||
|
value: '',
|
||||||
|
}
|
||||||
const transaction = getMockedTxServiceModel({ to: safeAddress2, value: '0' })
|
const transaction = getMockedTxServiceModel({ to: safeAddress2, value: '0' })
|
||||||
const userAddress = 'address1'
|
const userAddress = 'address1'
|
||||||
const cancellationTxs = List([cancelTx1])
|
const cancellationTxs = List([cancelTx1])
|
||||||
|
@ -776,7 +793,7 @@ describe('buildTx', () => {
|
||||||
})
|
})
|
||||||
const knownTokens = Map<string, Record<TokenProps> & Readonly<TokenProps>>()
|
const knownTokens = Map<string, Record<TokenProps> & Readonly<TokenProps>>()
|
||||||
knownTokens.set('0x00Df91984582e6e96288307E9c2f20b38C8FeCE9', token)
|
knownTokens.set('0x00Df91984582e6e96288307E9c2f20b38C8FeCE9', token)
|
||||||
const outgoingTxs = List([cancelTx1])
|
const outgoingTxs = [cancelTx1]
|
||||||
const safeInstance = makeSafe({ name: 'LOADED SAFE', address: safeAddress })
|
const safeInstance = makeSafe({ name: 'LOADED SAFE', address: safeAddress })
|
||||||
const expectedTx = makeTransaction({
|
const expectedTx = makeTransaction({
|
||||||
baseGas: 0,
|
baseGas: 0,
|
||||||
|
@ -826,7 +843,7 @@ describe('buildTx', () => {
|
||||||
outgoingTxs,
|
outgoingTxs,
|
||||||
safe: safeInstance,
|
safe: safeInstance,
|
||||||
tx: transaction,
|
tx: transaction,
|
||||||
txCode: null,
|
txCode: undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
// then
|
// then
|
||||||
|
|
|
@ -1,9 +1,18 @@
|
||||||
import { createAction } from 'redux-actions'
|
import { createAction } from 'redux-actions'
|
||||||
|
|
||||||
import { SafeRecordProps } from '../models/safe'
|
import { SafeOwner, SafeRecordProps } from '../models/safe'
|
||||||
|
import { List } from 'immutable'
|
||||||
|
import { makeOwner } from '../models/owner'
|
||||||
|
|
||||||
export const ADD_OR_UPDATE_SAFE = 'ADD_OR_UPDATE_SAFE'
|
export const ADD_OR_UPDATE_SAFE = 'ADD_OR_UPDATE_SAFE'
|
||||||
|
|
||||||
export const addOrUpdateSafe = createAction(ADD_OR_UPDATE_SAFE, (safe: SafeRecordProps) => ({
|
export const buildOwnersFrom = (names: string[], addresses: string[]): List<SafeOwner> => {
|
||||||
|
const owners = names.map((name, index) => makeOwner({ name, address: addresses[index] }))
|
||||||
|
|
||||||
|
return List(owners)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const addOrUpdateSafe = createAction(ADD_OR_UPDATE_SAFE, (safe: SafeRecordProps, loadedFromStorage = false) => ({
|
||||||
safe,
|
safe,
|
||||||
|
loadedFromStorage,
|
||||||
}))
|
}))
|
||||||
|
|
|
@ -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
|
|
|
@ -1,6 +1,6 @@
|
||||||
import axios, { AxiosResponse } from 'axios'
|
import axios, { AxiosResponse } from 'axios'
|
||||||
|
|
||||||
import { getAllTransactionsUriFrom, getTxServiceHost } from 'src/config'
|
import { getAllTransactionsUriFrom, getTxServiceUrl } from 'src/config'
|
||||||
import { checksumAddress } from 'src/utils/checksumAddress'
|
import { checksumAddress } from 'src/utils/checksumAddress'
|
||||||
import { Transaction } from '../../models/types/transactions.d'
|
import { Transaction } from '../../models/types/transactions.d'
|
||||||
|
|
||||||
|
@ -21,11 +21,11 @@ type TransactionDTO = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const getAllTransactionsUri = (safeAddress: string): string => {
|
const getAllTransactionsUri = (safeAddress: string): string => {
|
||||||
const host = getTxServiceHost()
|
const host = getTxServiceUrl()
|
||||||
const address = checksumAddress(safeAddress)
|
const address = checksumAddress(safeAddress)
|
||||||
const base = getAllTransactionsUriFrom(address)
|
const base = getAllTransactionsUriFrom(address)
|
||||||
|
|
||||||
return `${host}${base}`
|
return `${host}/${base}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchAllTransactions = async (
|
const fetchAllTransactions = async (
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import GnosisSafeSol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafe.json'
|
import GnosisSafeSol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafe.json'
|
||||||
import { List, Set, Map } from 'immutable'
|
import { List, Set, Map } from 'immutable'
|
||||||
|
import { Action, Dispatch } from 'redux'
|
||||||
|
import { AbiItem } from 'web3-utils'
|
||||||
|
|
||||||
import generateBatchRequests from 'src/logic/contracts/generateBatchRequests'
|
import generateBatchRequests from 'src/logic/contracts/generateBatchRequests'
|
||||||
import { getLocalSafe, getSafeName } from 'src/logic/safe/utils'
|
import { getLocalSafe, getSafeName } from 'src/logic/safe/utils'
|
||||||
|
@ -10,10 +12,8 @@ import addSafeOwner from 'src/logic/safe/store/actions/addSafeOwner'
|
||||||
import removeSafeOwner from 'src/logic/safe/store/actions/removeSafeOwner'
|
import removeSafeOwner from 'src/logic/safe/store/actions/removeSafeOwner'
|
||||||
import updateSafe from 'src/logic/safe/store/actions/updateSafe'
|
import updateSafe from 'src/logic/safe/store/actions/updateSafe'
|
||||||
import { makeOwner } from 'src/logic/safe/store/models/owner'
|
import { makeOwner } from 'src/logic/safe/store/models/owner'
|
||||||
|
|
||||||
import { checksumAddress } from 'src/utils/checksumAddress'
|
import { checksumAddress } from 'src/utils/checksumAddress'
|
||||||
import { ModulePair, SafeOwner, SafeRecordProps } from 'src/logic/safe/store/models/safe'
|
import { ModulePair, SafeOwner, SafeRecordProps } from 'src/logic/safe/store/models/safe'
|
||||||
import { Action, Dispatch } from 'redux'
|
|
||||||
import { SENTINEL_ADDRESS } from 'src/logic/contracts/safeContracts'
|
import { SENTINEL_ADDRESS } from 'src/logic/contracts/safeContracts'
|
||||||
import { AppReduxState } from 'src/store'
|
import { AppReduxState } from 'src/store'
|
||||||
import { latestMasterContractVersionSelector } from '../selectors'
|
import { latestMasterContractVersionSelector } from '../selectors'
|
||||||
|
@ -40,8 +40,8 @@ const buildOwnersFrom = (safeOwners: string[], localSafe?: SafeRecordProps): Lis
|
||||||
return List(ownersList)
|
return List(ownersList)
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildModulesLinkedList = (modules: string[] | undefined, nextModule: string): Array<ModulePair> | null => {
|
const buildModulesLinkedList = (modules?: string[], nextModule?: string): Array<ModulePair> | null => {
|
||||||
if (modules?.length) {
|
if (modules?.length && nextModule) {
|
||||||
return modules.map((moduleAddress, index, modules) => {
|
return modules.map((moduleAddress, index, modules) => {
|
||||||
const prevModule = modules[index + 1]
|
const prevModule = modules[index + 1]
|
||||||
return [moduleAddress, prevModule !== undefined ? prevModule : nextModule]
|
return [moduleAddress, prevModule !== undefined ? prevModule : nextModule]
|
||||||
|
@ -58,9 +58,9 @@ export const buildSafe = async (
|
||||||
const safeAddress = checksumAddress(safeAdd)
|
const safeAddress = checksumAddress(safeAdd)
|
||||||
|
|
||||||
const safeParams = ['getThreshold', 'nonce', 'VERSION', 'getOwners']
|
const safeParams = ['getThreshold', 'nonce', 'VERSION', 'getOwners']
|
||||||
const [[thresholdStr, nonceStr, currentVersion, remoteOwners], localSafe, ethBalance] = await Promise.all([
|
const [[, thresholdStr, nonceStr, currentVersion, remoteOwners = []], localSafe, ethBalance] = await Promise.all([
|
||||||
generateBatchRequests({
|
generateBatchRequests<[undefined, string | undefined, string | undefined, string | undefined, string[]]>({
|
||||||
abi: GnosisSafeSol.abi,
|
abi: GnosisSafeSol.abi as AbiItem[],
|
||||||
address: safeAddress,
|
address: safeAddress,
|
||||||
methods: safeParams,
|
methods: safeParams,
|
||||||
}),
|
}),
|
||||||
|
@ -81,7 +81,7 @@ export const buildSafe = async (
|
||||||
owners,
|
owners,
|
||||||
ethBalance,
|
ethBalance,
|
||||||
nonce,
|
nonce,
|
||||||
currentVersion,
|
currentVersion: currentVersion ?? '',
|
||||||
needsUpdate,
|
needsUpdate,
|
||||||
featuresEnabled,
|
featuresEnabled,
|
||||||
balances: Map(),
|
balances: Map(),
|
||||||
|
@ -104,9 +104,23 @@ export const checkAndUpdateSafe = (safeAdd: string) => async (dispatch: Dispatch
|
||||||
// TODO: 100 is an arbitrary large number, to avoid the need for pagination. But pagination must be properly handled
|
// TODO: 100 is an arbitrary large number, to avoid the need for pagination. But pagination must be properly handled
|
||||||
{ method: 'getModulesPaginated', args: [SENTINEL_ADDRESS, 100] },
|
{ method: 'getModulesPaginated', args: [SENTINEL_ADDRESS, 100] },
|
||||||
]
|
]
|
||||||
const [[remoteThreshold, remoteNonce, remoteOwners, modules], localSafe] = await Promise.all([
|
const [[, remoteThreshold, remoteNonce, remoteOwners, modules], localSafe] = await Promise.all([
|
||||||
generateBatchRequests({
|
generateBatchRequests<
|
||||||
abi: GnosisSafeSol.abi,
|
[
|
||||||
|
undefined,
|
||||||
|
string | undefined,
|
||||||
|
string | undefined,
|
||||||
|
string[],
|
||||||
|
(
|
||||||
|
| {
|
||||||
|
array: string[]
|
||||||
|
next: string
|
||||||
|
}
|
||||||
|
| undefined
|
||||||
|
),
|
||||||
|
]
|
||||||
|
>({
|
||||||
|
abi: GnosisSafeSol.abi as AbiItem[],
|
||||||
address: safeAddress,
|
address: safeAddress,
|
||||||
methods: safeParams,
|
methods: safeParams,
|
||||||
}),
|
}),
|
||||||
|
@ -123,6 +137,9 @@ export const checkAndUpdateSafe = (safeAdd: string) => async (dispatch: Dispatch
|
||||||
modules: buildModulesLinkedList(modules?.array, modules?.next),
|
modules: buildModulesLinkedList(modules?.array, modules?.next),
|
||||||
nonce: Number(remoteNonce),
|
nonce: Number(remoteNonce),
|
||||||
threshold: Number(remoteThreshold),
|
threshold: Number(remoteThreshold),
|
||||||
|
featuresEnabled: localSafe?.currentVersion
|
||||||
|
? enabledFeatures(localSafe?.currentVersion)
|
||||||
|
: localSafe?.featuresEnabled,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { List } from 'immutable'
|
import { List } from 'immutable'
|
||||||
|
|
||||||
import { buildSafeCreationTxUrl } from 'src/config'
|
import { buildSafeCreationTxUrl } from 'src/logic/safe/utils/buildSafeCreationTxUrl'
|
||||||
import { addOrUpdateTransactions } from './transactions/addOrUpdateTransactions'
|
import { addOrUpdateTransactions } from './transactions/addOrUpdateTransactions'
|
||||||
import { makeTransaction } from 'src/logic/safe/store/models/transaction'
|
import { makeTransaction } from 'src/logic/safe/store/models/transaction'
|
||||||
import { TransactionTypes, TransactionStatus } from 'src/logic/safe/store/models/types/transaction'
|
import { TransactionTypes, TransactionStatus } from 'src/logic/safe/store/models/types/transaction'
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { SafeRecordProps } from 'src/logic/safe/store/models/safe'
|
||||||
import { buildSafe } from 'src/logic/safe/store/reducer/safe'
|
import { buildSafe } from 'src/logic/safe/store/reducer/safe'
|
||||||
import { loadFromStorage } from 'src/utils/storage'
|
import { loadFromStorage } from 'src/utils/storage'
|
||||||
|
|
||||||
import { addSafe } from './addSafe'
|
import { addOrUpdateSafe } from './addOrUpdateSafe'
|
||||||
|
|
||||||
const loadSafesFromStorage = () => async (dispatch: Dispatch): Promise<void> => {
|
const loadSafesFromStorage = () => async (dispatch: Dispatch): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
|
@ -13,7 +13,7 @@ const loadSafesFromStorage = () => async (dispatch: Dispatch): Promise<void> =>
|
||||||
|
|
||||||
if (safes) {
|
if (safes) {
|
||||||
Object.values(safes).forEach((safeProps) => {
|
Object.values(safes).forEach((safeProps) => {
|
||||||
dispatch(addSafe(buildSafe(safeProps), true))
|
dispatch(addOrUpdateSafe(buildSafe(safeProps), true))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
import bn from 'bignumber.js'
|
import bn from 'bignumber.js'
|
||||||
import { List, Map } from 'immutable'
|
import { List, Map } from 'immutable'
|
||||||
|
import { Transaction, TransactionReceipt } from 'web3-core'
|
||||||
|
import { AbiItem } from 'web3-utils'
|
||||||
|
|
||||||
|
import { getNetworkInfo } from 'src/config'
|
||||||
import generateBatchRequests from 'src/logic/contracts/generateBatchRequests'
|
import generateBatchRequests from 'src/logic/contracts/generateBatchRequests'
|
||||||
import { ALTERNATIVE_TOKEN_ABI } from 'src/logic/tokens/utils/alternativeAbi'
|
import { ALTERNATIVE_TOKEN_ABI } from 'src/logic/tokens/utils/alternativeAbi'
|
||||||
import { web3ReadOnly } from 'src/logic/wallets/getWeb3'
|
import { web3ReadOnly } from 'src/logic/wallets/getWeb3'
|
||||||
|
@ -43,6 +46,7 @@ const buildIncomingTransactionFrom = ([tx, symbol, decimals, fee]: [
|
||||||
|
|
||||||
const batchIncomingTxsTokenDataRequest = (txs: IncomingTxServiceModel[]) => {
|
const batchIncomingTxsTokenDataRequest = (txs: IncomingTxServiceModel[]) => {
|
||||||
const batch = new web3ReadOnly.BatchRequest()
|
const batch = new web3ReadOnly.BatchRequest()
|
||||||
|
const { nativeCoin } = getNetworkInfo()
|
||||||
|
|
||||||
const whenTxsValues = txs.map((tx) => {
|
const whenTxsValues = txs.map((tx) => {
|
||||||
const methods = [
|
const methods = [
|
||||||
|
@ -52,8 +56,16 @@ const batchIncomingTxsTokenDataRequest = (txs: IncomingTxServiceModel[]) => {
|
||||||
{ method: 'getTransactionReceipt', args: [tx.transactionHash], type: 'eth' },
|
{ method: 'getTransactionReceipt', args: [tx.transactionHash], type: 'eth' },
|
||||||
]
|
]
|
||||||
|
|
||||||
return generateBatchRequests({
|
return generateBatchRequests<
|
||||||
abi: ALTERNATIVE_TOKEN_ABI,
|
[
|
||||||
|
IncomingTxServiceModel,
|
||||||
|
string | undefined,
|
||||||
|
string | undefined,
|
||||||
|
Transaction | undefined,
|
||||||
|
TransactionReceipt | undefined,
|
||||||
|
]
|
||||||
|
>({
|
||||||
|
abi: ALTERNATIVE_TOKEN_ABI as AbiItem[],
|
||||||
address: tx.tokenAddress,
|
address: tx.tokenAddress,
|
||||||
batch,
|
batch,
|
||||||
context: tx,
|
context: tx,
|
||||||
|
@ -64,11 +76,11 @@ const batchIncomingTxsTokenDataRequest = (txs: IncomingTxServiceModel[]) => {
|
||||||
batch.execute()
|
batch.execute()
|
||||||
|
|
||||||
return Promise.all(whenTxsValues).then((txsValues) =>
|
return Promise.all(whenTxsValues).then((txsValues) =>
|
||||||
txsValues.map(([tx, symbol, decimals, { gasPrice }, { gasUsed }]) => [
|
txsValues.map(([tx, symbol, decimals, ethTx, ethTxReceipt]) => [
|
||||||
tx,
|
tx,
|
||||||
symbol === null ? 'ETH' : symbol,
|
symbol ? symbol : nativeCoin.symbol,
|
||||||
decimals === null ? '18' : decimals,
|
decimals ? decimals : nativeCoin.decimals,
|
||||||
new bn(gasPrice).times(gasUsed),
|
new bn(ethTx?.gasPrice ?? 0).times(ethTxReceipt?.gasUsed ?? 0),
|
||||||
]),
|
]),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { fromJS, List, Map } from 'immutable'
|
import { fromJS, List, Map } from 'immutable'
|
||||||
|
|
||||||
import generateBatchRequests from 'src/logic/contracts/generateBatchRequests'
|
import generateBatchRequests from 'src/logic/contracts/generateBatchRequests'
|
||||||
import { TOKEN_REDUCER_ID } from 'src/logic/tokens/store/reducer/tokens'
|
import { TOKEN_REDUCER_ID, TokenState } from 'src/logic/tokens/store/reducer/tokens'
|
||||||
import { web3ReadOnly } from 'src/logic/wallets/getWeb3'
|
import { web3ReadOnly } from 'src/logic/wallets/getWeb3'
|
||||||
import { PROVIDER_REDUCER_ID } from 'src/logic/wallets/store/reducer/provider'
|
import { PROVIDER_REDUCER_ID } from 'src/logic/wallets/store/reducer/provider'
|
||||||
import { buildTx, isCancelTransaction } from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers'
|
import { buildTx, isCancelTransaction } from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers'
|
||||||
|
@ -59,8 +59,8 @@ export type SafeTransactionsType = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type OutgoingTxs = {
|
export type OutgoingTxs = {
|
||||||
cancellationTxs: any
|
cancellationTxs: Record<number, TxServiceModel>
|
||||||
outgoingTxs: any
|
outgoingTxs: TxServiceModel[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BatchProcessTxsProps = OutgoingTxs & {
|
export type BatchProcessTxsProps = OutgoingTxs & {
|
||||||
|
@ -97,12 +97,14 @@ const extractCancelAndOutgoingTxs = (safeAddress: string, outgoingTxs: TxService
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type BatchRequestReturnValues = [TxServiceModel, string | undefined]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Requests Contract's code for all the Contracts the Safe has interacted with
|
* Requests Contract's code for all the Contracts the Safe has interacted with
|
||||||
* @param transactions
|
* @param transactions
|
||||||
* @returns {Promise<[Promise<*[]>, Promise<*[]>, Promise<*[]>, Promise<*[]>, Promise<*[]>, Promise<*[]>, Promise<*[]>, Promise<*[]>, Promise<*[]>, Promise<*[]>]>}
|
* @returns {Promise<[Promise<*[]>, Promise<*[]>, Promise<*[]>, Promise<*[]>, Promise<*[]>, Promise<*[]>, Promise<*[]>, Promise<*[]>, Promise<*[]>, Promise<*[]>]>}
|
||||||
*/
|
*/
|
||||||
const batchRequestContractCode = (transactions: any[]): Promise<any[]> => {
|
const batchRequestContractCode = (transactions: TxServiceModel[]): Promise<BatchRequestReturnValues[]> => {
|
||||||
if (!transactions || !Array.isArray(transactions)) {
|
if (!transactions || !Array.isArray(transactions)) {
|
||||||
throw new Error('`transactions` must be provided in order to lookup information')
|
throw new Error('`transactions` must be provided in order to lookup information')
|
||||||
}
|
}
|
||||||
|
@ -110,7 +112,7 @@ const batchRequestContractCode = (transactions: any[]): Promise<any[]> => {
|
||||||
const batch = new web3ReadOnly.BatchRequest()
|
const batch = new web3ReadOnly.BatchRequest()
|
||||||
|
|
||||||
const whenTxsValues = transactions.map((tx) => {
|
const whenTxsValues = transactions.map((tx) => {
|
||||||
return generateBatchRequests({
|
return generateBatchRequests<BatchRequestReturnValues>({
|
||||||
abi: [],
|
abi: [],
|
||||||
address: tx.to,
|
address: tx.to,
|
||||||
batch,
|
batch,
|
||||||
|
@ -141,7 +143,7 @@ const batchProcessOutgoingTransactions = async ({
|
||||||
safe,
|
safe,
|
||||||
}: BatchProcessTxsProps): Promise<{
|
}: BatchProcessTxsProps): Promise<{
|
||||||
cancel: Record<string, Transaction>
|
cancel: Record<string, Transaction>
|
||||||
outgoing: Array<Transaction>
|
outgoing: Transaction[]
|
||||||
}> => {
|
}> => {
|
||||||
// cancellation transactions
|
// cancellation transactions
|
||||||
const cancelTxsValues = Object.values(cancellationTxs)
|
const cancelTxsValues = Object.values(cancellationTxs)
|
||||||
|
@ -193,9 +195,9 @@ export const loadOutgoingTransactions = async (safeAddress: string): Promise<Saf
|
||||||
return defaultResponse
|
return defaultResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
const knownTokens = state[TOKEN_REDUCER_ID]
|
const knownTokens: TokenState = state[TOKEN_REDUCER_ID]
|
||||||
const currentUser = state[PROVIDER_REDUCER_ID].get('account')
|
const currentUser: string = state[PROVIDER_REDUCER_ID].get('account')
|
||||||
const safe = state[SAFE_REDUCER_ID].getIn(['safes', safeAddress])
|
const safe: SafeRecord = state[SAFE_REDUCER_ID].getIn(['safes', safeAddress])
|
||||||
|
|
||||||
if (!safe) {
|
if (!safe) {
|
||||||
return defaultResponse
|
return defaultResponse
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { List, Map } from 'immutable'
|
import { List, Map } from 'immutable'
|
||||||
|
import { getNetworkInfo } from 'src/config'
|
||||||
import { TOKEN_REDUCER_ID } from 'src/logic/tokens/store/reducer/tokens'
|
import { TOKEN_REDUCER_ID, TokenState } from 'src/logic/tokens/store/reducer/tokens'
|
||||||
import {
|
import {
|
||||||
getERC20DecimalsAndSymbol,
|
getERC20DecimalsAndSymbol,
|
||||||
getERC721Symbol,
|
getERC721Symbol,
|
||||||
|
@ -78,15 +78,15 @@ export const isUpgradeTransaction = (tx: TxServiceModel): boolean => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isOutgoingTransaction = (tx: TxServiceModel, safeAddress: string): boolean => {
|
export const isOutgoingTransaction = (tx: TxServiceModel, safeAddress?: string): boolean => {
|
||||||
return !sameAddress(tx.to, safeAddress) && !isEmptyData(tx.data)
|
return !sameAddress(tx.to, safeAddress) && !isEmptyData(tx.data)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isCustomTransaction = async (
|
export const isCustomTransaction = async (
|
||||||
tx: TxServiceModel,
|
tx: TxServiceModel,
|
||||||
txCode: string | null,
|
txCode?: string,
|
||||||
safeAddress: string,
|
safeAddress?: string,
|
||||||
knownTokens: Map<string, Token>,
|
knownTokens?: TokenState,
|
||||||
): Promise<boolean> => {
|
): Promise<boolean> => {
|
||||||
const isOutgoing = isOutgoingTransaction(tx, safeAddress)
|
const isOutgoing = isOutgoingTransaction(tx, safeAddress)
|
||||||
const isErc20 = await isSendERC20Transaction(tx, txCode, knownTokens)
|
const isErc20 = await isSendERC20Transaction(tx, txCode, knownTokens)
|
||||||
|
@ -100,12 +100,13 @@ export const getRefundParams = async (
|
||||||
tx: TxServiceModel,
|
tx: TxServiceModel,
|
||||||
tokenInfo: (string) => Promise<{ decimals: number; symbol: string } | null>,
|
tokenInfo: (string) => Promise<{ decimals: number; symbol: string } | null>,
|
||||||
): Promise<RefundParams | null> => {
|
): Promise<RefundParams | null> => {
|
||||||
|
const { nativeCoin } = getNetworkInfo()
|
||||||
const txGasPrice = Number(tx.gasPrice)
|
const txGasPrice = Number(tx.gasPrice)
|
||||||
let refundParams: RefundParams | null = null
|
let refundParams: RefundParams | null = null
|
||||||
|
|
||||||
if (txGasPrice > 0) {
|
if (txGasPrice > 0) {
|
||||||
let refundSymbol = 'ETH'
|
let refundSymbol = nativeCoin.symbol
|
||||||
let refundDecimals = 18
|
let refundDecimals = nativeCoin.decimals
|
||||||
|
|
||||||
if (tx.gasToken !== ZERO_ADDRESS) {
|
if (tx.gasToken !== ZERO_ADDRESS) {
|
||||||
const gasToken = await tokenInfo(tx.gasToken)
|
const gasToken = await tokenInfo(tx.gasToken)
|
||||||
|
@ -161,7 +162,7 @@ export const getConfirmations = (tx: TxServiceModel): List<Confirmation> => {
|
||||||
export const isTransactionCancelled = (
|
export const isTransactionCancelled = (
|
||||||
tx: TxServiceModel,
|
tx: TxServiceModel,
|
||||||
outgoingTxs: Array<TxServiceModel>,
|
outgoingTxs: Array<TxServiceModel>,
|
||||||
cancellationTxs: { number: TxServiceModel },
|
cancellationTxs: Record<string, TxServiceModel>,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
return (
|
return (
|
||||||
// not executed
|
// not executed
|
||||||
|
@ -175,20 +176,20 @@ export const isTransactionCancelled = (
|
||||||
|
|
||||||
export const calculateTransactionStatus = (
|
export const calculateTransactionStatus = (
|
||||||
tx: Transaction,
|
tx: Transaction,
|
||||||
{ owners, threshold }: SafeRecord,
|
{ owners, threshold, nonce }: SafeRecord,
|
||||||
currentUser?: string | null,
|
currentUser?: string | null,
|
||||||
): TransactionStatusValues => {
|
): TransactionStatusValues => {
|
||||||
let txStatus
|
let txStatus
|
||||||
|
|
||||||
if (tx.isExecuted && tx.isSuccessful) {
|
if (tx.isExecuted && tx.isSuccessful) {
|
||||||
txStatus = TransactionStatus.SUCCESS
|
txStatus = TransactionStatus.SUCCESS
|
||||||
} else if (tx.cancelled) {
|
} else if (tx.cancelled || nonce > tx.nonce) {
|
||||||
txStatus = TransactionStatus.CANCELLED
|
txStatus = TransactionStatus.CANCELLED
|
||||||
} else if (tx.confirmations.size === threshold) {
|
} else if (tx.confirmations.size === threshold) {
|
||||||
txStatus = TransactionStatus.AWAITING_EXECUTION
|
txStatus = TransactionStatus.AWAITING_EXECUTION
|
||||||
} else if (tx.creationTx) {
|
} else if (tx.creationTx) {
|
||||||
txStatus = TransactionStatus.SUCCESS
|
txStatus = TransactionStatus.SUCCESS
|
||||||
} else if (!tx.confirmations.size || !!tx.isPending) {
|
} else if (!!tx.isPending) {
|
||||||
txStatus = TransactionStatus.PENDING
|
txStatus = TransactionStatus.PENDING
|
||||||
} else {
|
} else {
|
||||||
const userConfirmed = tx.confirmations.filter((conf) => conf.owner === currentUser).size === 1
|
const userConfirmed = tx.confirmations.filter((conf) => conf.owner === currentUser).size === 1
|
||||||
|
@ -230,7 +231,7 @@ export const calculateTransactionType = (tx: Transaction): TransactionTypeValues
|
||||||
|
|
||||||
export type BuildTx = BatchProcessTxsProps & {
|
export type BuildTx = BatchProcessTxsProps & {
|
||||||
tx: TxServiceModel
|
tx: TxServiceModel
|
||||||
txCode: string | null
|
txCode?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const buildTx = async ({
|
export const buildTx = async ({
|
||||||
|
@ -243,6 +244,7 @@ export const buildTx = async ({
|
||||||
txCode,
|
txCode,
|
||||||
}: BuildTx): Promise<Transaction> => {
|
}: BuildTx): Promise<Transaction> => {
|
||||||
const safeAddress = safe.address
|
const safeAddress = safe.address
|
||||||
|
const { nativeCoin } = getNetworkInfo()
|
||||||
const isModifySettingsTx = isModifySettingsTransaction(tx, safeAddress)
|
const isModifySettingsTx = isModifySettingsTransaction(tx, safeAddress)
|
||||||
const isTxCancelled = isTransactionCancelled(tx, outgoingTxs, cancellationTxs)
|
const isTxCancelled = isTransactionCancelled(tx, outgoingTxs, cancellationTxs)
|
||||||
const isSendERC721Tx = isSendERC721Transaction(tx, txCode, knownTokens)
|
const isSendERC721Tx = isSendERC721Transaction(tx, txCode, knownTokens)
|
||||||
|
@ -255,8 +257,8 @@ export const buildTx = async ({
|
||||||
const decodedParams = getDecodedParams(tx)
|
const decodedParams = getDecodedParams(tx)
|
||||||
const confirmations = getConfirmations(tx)
|
const confirmations = getConfirmations(tx)
|
||||||
|
|
||||||
let tokenDecimals = 18
|
let tokenDecimals = nativeCoin.decimals
|
||||||
let tokenSymbol = 'ETH'
|
let tokenSymbol = nativeCoin.symbol
|
||||||
try {
|
try {
|
||||||
if (isSendERC20Tx) {
|
if (isSendERC20Tx) {
|
||||||
const { decimals, symbol } = await getERC20DecimalsAndSymbol(tx.to)
|
const { decimals, symbol } = await getERC20DecimalsAndSymbol(tx.to)
|
||||||
|
|
|
@ -10,7 +10,6 @@ import { userAccountSelector } from 'src/logic/wallets/store/selectors'
|
||||||
import { getIncomingTxAmount } from 'src/routes/safe/components/Transactions/TxsTable/columns'
|
import { getIncomingTxAmount } from 'src/routes/safe/components/Transactions/TxsTable/columns'
|
||||||
import { grantedSelector } from 'src/routes/safe/container/selector'
|
import { grantedSelector } from 'src/routes/safe/container/selector'
|
||||||
import { ADD_INCOMING_TRANSACTIONS } from 'src/logic/safe/store/actions/addIncomingTransactions'
|
import { ADD_INCOMING_TRANSACTIONS } from 'src/logic/safe/store/actions/addIncomingTransactions'
|
||||||
import { ADD_SAFE } from 'src/logic/safe/store/actions/addSafe'
|
|
||||||
import { ADD_OR_UPDATE_TRANSACTIONS } from 'src/logic/safe/store/actions/transactions/addOrUpdateTransactions'
|
import { ADD_OR_UPDATE_TRANSACTIONS } from 'src/logic/safe/store/actions/transactions/addOrUpdateTransactions'
|
||||||
import updateSafe from 'src/logic/safe/store/actions/updateSafe'
|
import updateSafe from 'src/logic/safe/store/actions/updateSafe'
|
||||||
import {
|
import {
|
||||||
|
@ -20,8 +19,9 @@ import {
|
||||||
} from 'src/logic/safe/store/selectors'
|
} from 'src/logic/safe/store/selectors'
|
||||||
|
|
||||||
import { loadFromStorage, saveToStorage } from 'src/utils/storage'
|
import { loadFromStorage, saveToStorage } from 'src/utils/storage'
|
||||||
|
import { ADD_OR_UPDATE_SAFE } from '../actions/addOrUpdateSafe'
|
||||||
|
|
||||||
const watchedActions = [ADD_OR_UPDATE_TRANSACTIONS, ADD_INCOMING_TRANSACTIONS, ADD_SAFE]
|
const watchedActions = [ADD_OR_UPDATE_TRANSACTIONS, ADD_INCOMING_TRANSACTIONS, ADD_OR_UPDATE_SAFE]
|
||||||
|
|
||||||
const sendAwaitingTransactionNotification = async (
|
const sendAwaitingTransactionNotification = async (
|
||||||
dispatch,
|
dispatch,
|
||||||
|
@ -146,7 +146,7 @@ const notificationsMiddleware = (store) => (next) => async (action) => {
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case ADD_SAFE: {
|
case ADD_OR_UPDATE_SAFE: {
|
||||||
const state = store.getState()
|
const state = store.getState()
|
||||||
const { safe } = action.payload
|
const { safe } = action.payload
|
||||||
const currentSafeAddress = safeParamAddressFromStateSelector(state) || safe.address
|
const currentSafeAddress = safeParamAddressFromStateSelector(state) || safe.address
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
import { addAddressBookEntry } from 'src/logic/addressBook/store/actions/addAddressBookEntry'
|
|
||||||
import { saveDefaultSafe, saveSafes } from 'src/logic/safe/utils'
|
import { saveDefaultSafe, saveSafes } from 'src/logic/safe/utils'
|
||||||
import { tokensSelector } from 'src/logic/tokens/store/selectors'
|
import { tokensSelector } from 'src/logic/tokens/store/selectors'
|
||||||
import { saveActiveTokens } from 'src/logic/tokens/utils/tokensStorage'
|
import { saveActiveTokens } from 'src/logic/tokens/utils/tokensStorage'
|
||||||
import { ACTIVATE_TOKEN_FOR_ALL_SAFES } from 'src/logic/safe/store/actions/activateTokenForAllSafes'
|
import { ACTIVATE_TOKEN_FOR_ALL_SAFES } from 'src/logic/safe/store/actions/activateTokenForAllSafes'
|
||||||
import { ADD_SAFE } from 'src/logic/safe/store/actions/addSafe'
|
|
||||||
import { ADD_SAFE_OWNER } from 'src/logic/safe/store/actions/addSafeOwner'
|
import { ADD_SAFE_OWNER } from 'src/logic/safe/store/actions/addSafeOwner'
|
||||||
import { EDIT_SAFE_OWNER } from 'src/logic/safe/store/actions/editSafeOwner'
|
import { EDIT_SAFE_OWNER } from 'src/logic/safe/store/actions/editSafeOwner'
|
||||||
import { REMOVE_SAFE } from 'src/logic/safe/store/actions/removeSafe'
|
import { REMOVE_SAFE } from 'src/logic/safe/store/actions/removeSafe'
|
||||||
|
@ -14,17 +12,13 @@ import { UPDATE_SAFE } from 'src/logic/safe/store/actions/updateSafe'
|
||||||
import { UPDATE_TOKENS_LIST } from 'src/logic/safe/store/actions/updateTokensList'
|
import { UPDATE_TOKENS_LIST } from 'src/logic/safe/store/actions/updateTokensList'
|
||||||
import { UPDATE_ASSETS_LIST } from 'src/logic/safe/store/actions/updateAssetsList'
|
import { UPDATE_ASSETS_LIST } from 'src/logic/safe/store/actions/updateAssetsList'
|
||||||
import { getActiveTokensAddressesForAllSafes, safesMapSelector } from 'src/logic/safe/store/selectors'
|
import { getActiveTokensAddressesForAllSafes, safesMapSelector } from 'src/logic/safe/store/selectors'
|
||||||
import { checksumAddress } from 'src/utils/checksumAddress'
|
|
||||||
import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
|
||||||
import { addOrUpdateAddressBookEntry } from 'src/logic/addressBook/store/actions/addOrUpdateAddressBookEntry'
|
|
||||||
import { checkIfEntryWasDeletedFromAddressBook, isValidAddressBookName } from 'src/logic/addressBook/utils'
|
|
||||||
import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
|
|
||||||
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
|
||||||
import { updateAddressBookEntry } from 'src/logic/addressBook/store/actions/updateAddressBookEntry'
|
|
||||||
import { ADD_OR_UPDATE_SAFE } from 'src/logic/safe/store/actions/addOrUpdateSafe'
|
import { ADD_OR_UPDATE_SAFE } from 'src/logic/safe/store/actions/addOrUpdateSafe'
|
||||||
|
import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
||||||
|
import { checksumAddress } from 'src/utils/checksumAddress'
|
||||||
|
import { isValidAddressBookName } from 'src/logic/addressBook/utils'
|
||||||
|
import { addOrUpdateAddressBookEntry } from 'src/logic/addressBook/store/actions/addOrUpdateAddressBookEntry'
|
||||||
|
|
||||||
const watchedActions = [
|
const watchedActions = [
|
||||||
ADD_SAFE,
|
|
||||||
UPDATE_SAFE,
|
UPDATE_SAFE,
|
||||||
REMOVE_SAFE,
|
REMOVE_SAFE,
|
||||||
ADD_OR_UPDATE_SAFE,
|
ADD_OR_UPDATE_SAFE,
|
||||||
|
@ -60,7 +54,6 @@ const safeStorageMware = (store) => (next) => async (action) => {
|
||||||
const state = store.getState()
|
const state = store.getState()
|
||||||
const { dispatch } = store
|
const { dispatch } = store
|
||||||
const safes = safesMapSelector(state)
|
const safes = safesMapSelector(state)
|
||||||
const addressBook = addressBookSelector(state)
|
|
||||||
await saveSafes(safes.toJSON())
|
await saveSafes(safes.toJSON())
|
||||||
|
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
|
@ -68,43 +61,6 @@ const safeStorageMware = (store) => (next) => async (action) => {
|
||||||
recalculateActiveTokens(state)
|
recalculateActiveTokens(state)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case ADD_SAFE: {
|
|
||||||
const { safe, loadedFromStorage } = action.payload
|
|
||||||
const safeAlreadyLoaded =
|
|
||||||
loadedFromStorage || safes.find((safeIterator) => sameAddress(safeIterator.address, safe.address))
|
|
||||||
|
|
||||||
safe.owners.forEach((owner) => {
|
|
||||||
const checksumEntry = makeAddressBookEntry({ address: checksumAddress(owner.address), name: owner.name })
|
|
||||||
|
|
||||||
const ownerWasAlreadyInAddressBook = checkIfEntryWasDeletedFromAddressBook(
|
|
||||||
checksumEntry,
|
|
||||||
addressBook,
|
|
||||||
safeAlreadyLoaded,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!ownerWasAlreadyInAddressBook) {
|
|
||||||
dispatch(addAddressBookEntry(checksumEntry, { notifyEntryUpdate: false }))
|
|
||||||
}
|
|
||||||
const addressAlreadyExists = addressBook.find((entry) => sameAddress(entry.address, checksumEntry.address))
|
|
||||||
if (isValidAddressBookName(checksumEntry.name) && addressAlreadyExists) {
|
|
||||||
dispatch(updateAddressBookEntry(checksumEntry))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const safeWasAlreadyInAddressBook = checkIfEntryWasDeletedFromAddressBook(
|
|
||||||
{ address: safe.address, name: safe.name },
|
|
||||||
addressBook,
|
|
||||||
safeAlreadyLoaded,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!safeWasAlreadyInAddressBook) {
|
|
||||||
dispatch(
|
|
||||||
addAddressBookEntry(makeAddressBookEntry({ address: safe.address, name: safe.name }), {
|
|
||||||
notifyEntryUpdate: true,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case ADD_OR_UPDATE_SAFE: {
|
case ADD_OR_UPDATE_SAFE: {
|
||||||
const { safe } = action.payload
|
const { safe } = action.payload
|
||||||
safe.owners.forEach((owner) => {
|
safe.owners.forEach((owner) => {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { List, Map, Record, RecordOf, Set } from 'immutable'
|
import { List, Map, Record, RecordOf, Set } from 'immutable'
|
||||||
|
import { FEATURES } from 'src/config/networks/network.d'
|
||||||
|
|
||||||
export type SafeOwner = {
|
export type SafeOwner = {
|
||||||
name: string
|
name: string
|
||||||
|
@ -24,7 +25,7 @@ export type SafeRecordProps = {
|
||||||
recurringUser?: boolean
|
recurringUser?: boolean
|
||||||
currentVersion: string
|
currentVersion: string
|
||||||
needsUpdate: boolean
|
needsUpdate: boolean
|
||||||
featuresEnabled: Array<string>
|
featuresEnabled: Array<FEATURES>
|
||||||
}
|
}
|
||||||
|
|
||||||
const makeSafe = Record<SafeRecordProps>({
|
const makeSafe = Record<SafeRecordProps>({
|
||||||
|
|
|
@ -31,6 +31,7 @@ export const makeTransaction = Record<TransactionProps>({
|
||||||
isCancellationTx: false,
|
isCancellationTx: false,
|
||||||
isCollectibleTransfer: false,
|
isCollectibleTransfer: false,
|
||||||
isExecuted: false,
|
isExecuted: false,
|
||||||
|
isPending: false,
|
||||||
isSuccessful: true,
|
isSuccessful: true,
|
||||||
isTokenTransfer: false,
|
isTokenTransfer: false,
|
||||||
masterCopy: '',
|
masterCopy: '',
|
||||||
|
|
|
@ -2,7 +2,6 @@ import { Map, Set, List } from 'immutable'
|
||||||
import { handleActions } from 'redux-actions'
|
import { handleActions } from 'redux-actions'
|
||||||
|
|
||||||
import { ACTIVATE_TOKEN_FOR_ALL_SAFES } from 'src/logic/safe/store/actions/activateTokenForAllSafes'
|
import { ACTIVATE_TOKEN_FOR_ALL_SAFES } from 'src/logic/safe/store/actions/activateTokenForAllSafes'
|
||||||
import { ADD_SAFE, buildOwnersFrom } from 'src/logic/safe/store/actions/addSafe'
|
|
||||||
import { ADD_SAFE_OWNER } from 'src/logic/safe/store/actions/addSafeOwner'
|
import { ADD_SAFE_OWNER } from 'src/logic/safe/store/actions/addSafeOwner'
|
||||||
import { EDIT_SAFE_OWNER } from 'src/logic/safe/store/actions/editSafeOwner'
|
import { EDIT_SAFE_OWNER } from 'src/logic/safe/store/actions/editSafeOwner'
|
||||||
import { REMOVE_SAFE } from 'src/logic/safe/store/actions/removeSafe'
|
import { REMOVE_SAFE } from 'src/logic/safe/store/actions/removeSafe'
|
||||||
|
@ -17,7 +16,7 @@ import { makeOwner } from 'src/logic/safe/store/models/owner'
|
||||||
import makeSafe, { SafeRecordProps } from 'src/logic/safe/store/models/safe'
|
import makeSafe, { SafeRecordProps } from 'src/logic/safe/store/models/safe'
|
||||||
import { checksumAddress } from 'src/utils/checksumAddress'
|
import { checksumAddress } from 'src/utils/checksumAddress'
|
||||||
import { SafeReducerMap } from 'src/routes/safe/store/reducer/types/safe'
|
import { SafeReducerMap } from 'src/routes/safe/store/reducer/types/safe'
|
||||||
import { ADD_OR_UPDATE_SAFE } from 'src/logic/safe/store/actions/addOrUpdateSafe'
|
import { ADD_OR_UPDATE_SAFE, buildOwnersFrom } from 'src/logic/safe/store/actions/addOrUpdateSafe'
|
||||||
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
||||||
|
|
||||||
export const SAFE_REDUCER_ID = 'safes'
|
export const SAFE_REDUCER_ID = 'safes'
|
||||||
|
@ -99,19 +98,7 @@ export default handleActions(
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
[ADD_SAFE]: (state: SafeReducerMap, action) => {
|
|
||||||
const { safe } = action.payload
|
|
||||||
|
|
||||||
// if you add a new Safe it needs to be set as a record
|
|
||||||
// in case of update it shouldn't, because a record would be initialized
|
|
||||||
// with initial props and it would overwrite existing ones
|
|
||||||
|
|
||||||
if (state.hasIn(['safes', safe.address])) {
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
|
|
||||||
return state.setIn(['safes', safe.address], makeSafe(safe))
|
|
||||||
},
|
|
||||||
[ADD_OR_UPDATE_SAFE]: (state: SafeReducerMap, action) => {
|
[ADD_OR_UPDATE_SAFE]: (state: SafeReducerMap, action) => {
|
||||||
const { safe } = action.payload
|
const { safe } = action.payload
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@ export default handleActions(
|
||||||
if (stateTransactionsList) {
|
if (stateTransactionsList) {
|
||||||
const txsToStore = stateTransactionsList.withMutations((txsList) => {
|
const txsToStore = stateTransactionsList.withMutations((txsList) => {
|
||||||
transactions.forEach((updateTx) => {
|
transactions.forEach((updateTx) => {
|
||||||
const storedTxIndex = txsList.findIndex((txIterator) => txIterator.nonce === updateTx.nonce)
|
const storedTxIndex = txsList.findIndex((txIterator) => txIterator.safeTxHash === updateTx.safeTxHash)
|
||||||
|
|
||||||
if (storedTxIndex !== -1) {
|
if (storedTxIndex !== -1) {
|
||||||
// Update
|
// Update
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { getIncomingTxServiceUriTo, getTxServiceHost } from 'src/config'
|
import { getIncomingTxServiceUriTo, getTxServiceUrl } from 'src/config'
|
||||||
import { checksumAddress } from 'src/utils/checksumAddress'
|
import { checksumAddress } from 'src/utils/checksumAddress'
|
||||||
|
|
||||||
export const buildIncomingTxServiceUrl = (safeAddress: string): string => {
|
export const buildIncomingTxServiceUrl = (safeAddress: string): string => {
|
||||||
const host = getTxServiceHost()
|
const host = getTxServiceUrl()
|
||||||
const address = checksumAddress(safeAddress)
|
const address = checksumAddress(safeAddress)
|
||||||
const base = getIncomingTxServiceUriTo(address)
|
const base = getIncomingTxServiceUriTo(address)
|
||||||
|
|
||||||
return `${host}${base}`
|
return `${host}/${base}`
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
import { GnosisSafe } from 'src/types/contracts/GnosisSafe.d'
|
import { GnosisSafe } from 'src/types/contracts/GnosisSafe.d'
|
||||||
import { getTxServiceHost, getTxServiceUriFrom } from 'src/config'
|
import { getTxServiceUrl, getTxServiceUriFrom } from 'src/config'
|
||||||
import { checksumAddress } from 'src/utils/checksumAddress'
|
import { checksumAddress } from 'src/utils/checksumAddress'
|
||||||
|
|
||||||
const calculateBodyFrom = async (
|
const calculateBodyFrom = async (
|
||||||
|
@ -45,10 +45,10 @@ const calculateBodyFrom = async (
|
||||||
}
|
}
|
||||||
|
|
||||||
export const buildTxServiceUrl = (safeAddress: string): string => {
|
export const buildTxServiceUrl = (safeAddress: string): string => {
|
||||||
const host = getTxServiceHost()
|
const host = getTxServiceUrl()
|
||||||
const address = checksumAddress(safeAddress)
|
const address = checksumAddress(safeAddress)
|
||||||
const base = getTxServiceUriFrom(address)
|
const base = getTxServiceUriFrom(address)
|
||||||
return `${host}${base}?has_confirmations=True`
|
return `${host}/${base}?has_confirmations=True`
|
||||||
}
|
}
|
||||||
|
|
||||||
const SUCCESS_STATUS = 201 // CREATED status
|
const SUCCESS_STATUS = 201 // CREATED status
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { getTxServiceUrl, getSafeCreationTxUri } from 'src/config'
|
||||||
|
import { checksumAddress } from 'src/utils/checksumAddress'
|
||||||
|
|
||||||
|
export const buildSafeCreationTxUrl = (safeAddress: string): string => {
|
||||||
|
const host = getTxServiceUrl()
|
||||||
|
const address = checksumAddress(safeAddress)
|
||||||
|
const base = getSafeCreationTxUri(address)
|
||||||
|
|
||||||
|
return `${host}/${base}`
|
||||||
|
}
|
|
@ -3,15 +3,24 @@ import semverSatisfies from 'semver/functions/satisfies'
|
||||||
import semverValid from 'semver/functions/valid'
|
import semverValid from 'semver/functions/valid'
|
||||||
import { GnosisSafe } from 'src/types/contracts/GnosisSafe.d'
|
import { GnosisSafe } from 'src/types/contracts/GnosisSafe.d'
|
||||||
|
|
||||||
import { getSafeLastVersion } from 'src/config'
|
|
||||||
import { getGnosisSafeInstanceAt, getSafeMasterContract } from 'src/logic/contracts/safeContracts'
|
import { getGnosisSafeInstanceAt, getSafeMasterContract } from 'src/logic/contracts/safeContracts'
|
||||||
|
import { LATEST_SAFE_VERSION } from 'src/utils/constants'
|
||||||
|
import { getNetworkConfigDisabledFeatures } from 'src/config'
|
||||||
|
import { FEATURES } from 'src/config/networks/network.d'
|
||||||
|
|
||||||
export const FEATURES = [
|
type FeatureConfigByVersion = {
|
||||||
{ name: 'ERC721', validVersion: '>=1.1.1' },
|
name: FEATURES
|
||||||
{ name: 'ERC1155', validVersion: '>=1.1.1' },
|
validVersion?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const FEATURES_BY_VERSION: FeatureConfigByVersion[] = [
|
||||||
|
{ name: FEATURES.ERC721, validVersion: '>=1.1.1' },
|
||||||
|
{ name: FEATURES.ERC1155, validVersion: '>=1.1.1' },
|
||||||
|
{ name: FEATURES.SAFE_APPS },
|
||||||
|
{ name: FEATURES.CONTRACT_INTERACTION },
|
||||||
]
|
]
|
||||||
|
|
||||||
type Feature = typeof FEATURES[number]
|
type Feature = typeof FEATURES_BY_VERSION[number]
|
||||||
|
|
||||||
export const safeNeedsUpdate = (currentVersion?: string, latestVersion?: string): boolean => {
|
export const safeNeedsUpdate = (currentVersion?: string, latestVersion?: string): boolean => {
|
||||||
if (!currentVersion || !latestVersion) {
|
if (!currentVersion || !latestVersion) {
|
||||||
|
@ -27,13 +36,19 @@ export const safeNeedsUpdate = (currentVersion?: string, latestVersion?: string)
|
||||||
export const getCurrentSafeVersion = (gnosisSafeInstance: GnosisSafe): Promise<string> =>
|
export const getCurrentSafeVersion = (gnosisSafeInstance: GnosisSafe): Promise<string> =>
|
||||||
gnosisSafeInstance.methods.VERSION().call()
|
gnosisSafeInstance.methods.VERSION().call()
|
||||||
|
|
||||||
export const enabledFeatures = (version: string): string[] =>
|
const checkFeatureEnabledByVersion = (featureConfig: FeatureConfigByVersion, version: string) => {
|
||||||
FEATURES.reduce((acc: string[], feature: Feature) => {
|
return featureConfig.validVersion ? semverSatisfies(version, featureConfig.validVersion) : true
|
||||||
if (semverSatisfies(version, feature.validVersion)) {
|
}
|
||||||
|
|
||||||
|
export const enabledFeatures = (version?: string): FEATURES[] => {
|
||||||
|
const disabledFeatures = getNetworkConfigDisabledFeatures()
|
||||||
|
return FEATURES_BY_VERSION.reduce((acc: FEATURES[], feature: Feature) => {
|
||||||
|
if (!disabledFeatures.includes(feature.name) && version && checkFeatureEnabledByVersion(feature, version)) {
|
||||||
acc.push(feature.name)
|
acc.push(feature.name)
|
||||||
}
|
}
|
||||||
return acc
|
return acc
|
||||||
}, [])
|
}, [])
|
||||||
|
}
|
||||||
|
|
||||||
interface SafeVersionInfo {
|
interface SafeVersionInfo {
|
||||||
current: string
|
current: string
|
||||||
|
@ -60,11 +75,11 @@ export const getCurrentMasterContractLastVersion = async (): Promise<string> =>
|
||||||
const safeMaster = await getSafeMasterContract()
|
const safeMaster = await getSafeMasterContract()
|
||||||
let safeMasterVersion
|
let safeMasterVersion
|
||||||
try {
|
try {
|
||||||
safeMasterVersion = await safeMaster.VERSION()
|
safeMasterVersion = await safeMaster.methods.VERSION().call()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Default in case that it's not possible to obtain the version from the contract, returns a hardcoded value or an
|
// Default in case that it's not possible to obtain the version from the contract, returns a hardcoded value or an
|
||||||
// env variable
|
// env variable
|
||||||
safeMasterVersion = getSafeLastVersion()
|
safeMasterVersion = LATEST_SAFE_VERSION
|
||||||
}
|
}
|
||||||
return safeMasterVersion
|
return safeMasterVersion
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
import axios, { AxiosResponse } from 'axios'
|
||||||
|
|
||||||
|
import { getTxServiceUrl } from 'src/config'
|
||||||
|
|
||||||
|
export type TokenResult = {
|
||||||
|
address: string
|
||||||
|
decimals?: number
|
||||||
|
logoUri: string
|
||||||
|
name: string
|
||||||
|
symbol: string
|
||||||
|
type: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fetchErc20AndErc721AssetsList = async (): Promise<AxiosResponse<{ results: TokenResult[] }>> => {
|
||||||
|
const apiUrl = getTxServiceUrl()
|
||||||
|
|
||||||
|
const url = `${apiUrl}/tokens/`
|
||||||
|
|
||||||
|
return axios.get<{ results: TokenResult[] }>(url, {
|
||||||
|
params: {
|
||||||
|
limit: 3000,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
import axios, { AxiosResponse } from 'axios'
|
||||||
|
|
||||||
|
import { getTxServiceUrl } from 'src/config'
|
||||||
|
|
||||||
|
export type CollectibleResult = {
|
||||||
|
address: string
|
||||||
|
description: string | null
|
||||||
|
id: string
|
||||||
|
imageUri: string | null
|
||||||
|
logoUri: string
|
||||||
|
metadata: Record<string, unknown>
|
||||||
|
name: string | null
|
||||||
|
tokenName: string
|
||||||
|
tokenSymbol: string
|
||||||
|
uri: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fetchSafeCollectibles = async (safeAddress: string): Promise<AxiosResponse<CollectibleResult[]>> => {
|
||||||
|
const apiUrl = getTxServiceUrl()
|
||||||
|
|
||||||
|
const url = `${apiUrl}/safes/${safeAddress}/collectibles/`
|
||||||
|
|
||||||
|
return axios.get(url)
|
||||||
|
}
|
|
@ -1,16 +0,0 @@
|
||||||
import axios from 'axios'
|
|
||||||
|
|
||||||
import { getRelayUrl } from 'src/config/index'
|
|
||||||
|
|
||||||
const fetchToken = (tokenAddress) => {
|
|
||||||
const apiUrl = getRelayUrl()
|
|
||||||
const url = `${apiUrl}/tokens/`
|
|
||||||
|
|
||||||
return axios.get(url, {
|
|
||||||
params: {
|
|
||||||
address: tokenAddress,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export default fetchToken
|
|
|
@ -1,16 +1,17 @@
|
||||||
import axios from 'axios'
|
import axios, { AxiosResponse } from 'axios'
|
||||||
|
|
||||||
import { getTxServiceHost } from 'src/config/index'
|
import { getTxServiceUrl } from 'src/config'
|
||||||
|
import { TokenProps } from 'src/logic/tokens/store/model/token'
|
||||||
|
|
||||||
const fetchTokenBalanceList = (safeAddress) => {
|
type BalanceResult = {
|
||||||
const apiUrl = getTxServiceHost()
|
tokenAddress: string
|
||||||
const url = `${apiUrl}safes/${safeAddress}/balances/`
|
token: TokenProps
|
||||||
|
balance: string
|
||||||
return axios.get(url, {
|
|
||||||
params: {
|
|
||||||
limit: 3000,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default fetchTokenBalanceList
|
export const fetchTokenBalanceList = (safeAddress: string): Promise<AxiosResponse<{ results: BalanceResult[] }>> => {
|
||||||
|
const apiUrl = getTxServiceUrl()
|
||||||
|
const url = `${apiUrl}/safes/${safeAddress}/balances/`
|
||||||
|
|
||||||
|
return axios.get(url)
|
||||||
|
}
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
import axios from 'axios'
|
|
||||||
|
|
||||||
import { getRelayUrl } from 'src/config/index'
|
|
||||||
|
|
||||||
const fetchTokenList = () => {
|
|
||||||
const apiUrl = getRelayUrl()
|
|
||||||
const url = `${apiUrl}tokens/`
|
|
||||||
|
|
||||||
return axios.get(url, {
|
|
||||||
params: {
|
|
||||||
limit: 3000,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export default fetchTokenList
|
|
|
@ -1,2 +1,3 @@
|
||||||
export { default as fetchTokenList } from './fetchTokenList'
|
export { fetchErc20AndErc721AssetsList } from './fetchErc20AndErc721AssetsList'
|
||||||
export { default as fetchToken } from './fetchToken'
|
export { fetchSafeCollectibles } from './fetchSafeCollectibles'
|
||||||
|
export { fetchTokenBalanceList } from './fetchTokenBalanceList'
|
||||||
|
|
|
@ -3,15 +3,12 @@ import { List, Map } from 'immutable'
|
||||||
import { batch } from 'react-redux'
|
import { batch } from 'react-redux'
|
||||||
import { Dispatch } from 'redux'
|
import { Dispatch } from 'redux'
|
||||||
|
|
||||||
import fetchTokenCurrenciesBalances, {
|
import {
|
||||||
|
fetchTokenCurrenciesBalances,
|
||||||
BalanceEndpoint,
|
BalanceEndpoint,
|
||||||
} from 'src/logic/currencyValues/api/fetchTokenCurrenciesBalances'
|
} from 'src/logic/currencyValues/api/fetchTokenCurrenciesBalances'
|
||||||
import { setCurrencyBalances } from 'src/logic/currencyValues/store/actions/setCurrencyBalances'
|
import { setCurrencyBalances } from 'src/logic/currencyValues/store/actions/setCurrencyBalances'
|
||||||
import {
|
import { CurrencyRateValueRecord, makeBalanceCurrency } from 'src/logic/currencyValues/store/model/currencyValues'
|
||||||
AVAILABLE_CURRENCIES,
|
|
||||||
CurrencyRateValueRecord,
|
|
||||||
makeBalanceCurrency,
|
|
||||||
} from 'src/logic/currencyValues/store/model/currencyValues'
|
|
||||||
import addTokens from 'src/logic/tokens/store/actions/saveTokens'
|
import addTokens from 'src/logic/tokens/store/actions/saveTokens'
|
||||||
import { makeToken, Token } from 'src/logic/tokens/store/model/token'
|
import { makeToken, Token } from 'src/logic/tokens/store/model/token'
|
||||||
import { TokenState } from 'src/logic/tokens/store/reducer/tokens'
|
import { TokenState } from 'src/logic/tokens/store/reducer/tokens'
|
||||||
|
@ -43,7 +40,7 @@ interface ExtractedData {
|
||||||
|
|
||||||
const extractDataFromResult = (currentTokens: TokenState) => (
|
const extractDataFromResult = (currentTokens: TokenState) => (
|
||||||
acc: ExtractedData,
|
acc: ExtractedData,
|
||||||
{ balance, balanceUsd, token, tokenAddress }: BalanceEndpoint,
|
{ balance, fiatBalance, fiatCode, token, tokenAddress }: BalanceEndpoint,
|
||||||
): ExtractedData => {
|
): ExtractedData => {
|
||||||
if (tokenAddress === null) {
|
if (tokenAddress === null) {
|
||||||
acc.ethBalance = humanReadableValue(balance, 18)
|
acc.ethBalance = humanReadableValue(balance, 18)
|
||||||
|
@ -57,10 +54,10 @@ const extractDataFromResult = (currentTokens: TokenState) => (
|
||||||
|
|
||||||
acc.currencyList = acc.currencyList.push(
|
acc.currencyList = acc.currencyList.push(
|
||||||
makeBalanceCurrency({
|
makeBalanceCurrency({
|
||||||
currencyName: balanceUsd ? AVAILABLE_CURRENCIES.USD : undefined,
|
currencyName: fiatCode,
|
||||||
tokenAddress,
|
tokenAddress,
|
||||||
balanceInBaseCurrency: balanceUsd,
|
balanceInBaseCurrency: fiatBalance,
|
||||||
balanceInSelectedCurrency: balanceUsd,
|
balanceInSelectedCurrency: fiatBalance,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -3,12 +3,13 @@ import HumanFriendlyToken from '@gnosis.pm/util-contracts/build/contracts/HumanF
|
||||||
import ERC20Detailed from '@openzeppelin/contracts/build/contracts/ERC20Detailed.json'
|
import ERC20Detailed from '@openzeppelin/contracts/build/contracts/ERC20Detailed.json'
|
||||||
import ERC721 from '@openzeppelin/contracts/build/contracts/ERC721.json'
|
import ERC721 from '@openzeppelin/contracts/build/contracts/ERC721.json'
|
||||||
import { List } from 'immutable'
|
import { List } from 'immutable'
|
||||||
import contract from 'truffle-contract'
|
import contract from '@truffle/contract/index.js'
|
||||||
|
import { AbiItem } from 'web3-utils'
|
||||||
|
|
||||||
import saveTokens from './saveTokens'
|
import saveTokens from './saveTokens'
|
||||||
|
|
||||||
import generateBatchRequests from 'src/logic/contracts/generateBatchRequests'
|
import generateBatchRequests from 'src/logic/contracts/generateBatchRequests'
|
||||||
import { fetchTokenList } from 'src/logic/tokens/api'
|
import { fetchErc20AndErc721AssetsList } from 'src/logic/tokens/api'
|
||||||
import { makeToken, Token } from 'src/logic/tokens/store/model/token'
|
import { makeToken, Token } from 'src/logic/tokens/store/model/token'
|
||||||
import { tokensSelector } from 'src/logic/tokens/store/selectors'
|
import { tokensSelector } from 'src/logic/tokens/store/selectors'
|
||||||
import { getWeb3 } from 'src/logic/wallets/getWeb3'
|
import { getWeb3 } from 'src/logic/wallets/getWeb3'
|
||||||
|
@ -53,8 +54,8 @@ export const containsMethodByHash = async (contractAddress: string, methodHash:
|
||||||
}
|
}
|
||||||
|
|
||||||
const getTokenValues = (tokenAddress) =>
|
const getTokenValues = (tokenAddress) =>
|
||||||
generateBatchRequests({
|
generateBatchRequests<[undefined, string | undefined, string | undefined, string | undefined]>({
|
||||||
abi: ERC20Detailed.abi,
|
abi: ERC20Detailed.abi as AbiItem[],
|
||||||
address: tokenAddress,
|
address: tokenAddress,
|
||||||
methods: ['decimals', 'name', 'symbol'],
|
methods: ['decimals', 'name', 'symbol'],
|
||||||
})
|
})
|
||||||
|
@ -69,7 +70,7 @@ export const getTokenInfos = async (tokenAddress: string): Promise<Token | undef
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise we fetch it, save it to the store and return it
|
// Otherwise we fetch it, save it to the store and return it
|
||||||
const [tokenDecimals, tokenName, tokenSymbol] = await getTokenValues(tokenAddress)
|
const [, tokenDecimals, tokenName, tokenSymbol] = await getTokenValues(tokenAddress)
|
||||||
|
|
||||||
if (tokenDecimals === null) {
|
if (tokenDecimals === null) {
|
||||||
return undefined
|
return undefined
|
||||||
|
@ -98,13 +99,15 @@ export const fetchTokens = () => async (
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: { results: tokenList },
|
data: { results: tokenList },
|
||||||
} = await fetchTokenList()
|
} = await fetchErc20AndErc721AssetsList()
|
||||||
|
|
||||||
if (currentSavedTokens && currentSavedTokens.size === tokenList.length) {
|
const erc20Tokens = tokenList.filter((token) => token.type.toLowerCase() === 'erc20')
|
||||||
|
|
||||||
|
if (currentSavedTokens?.size === erc20Tokens.length) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const tokens = List(tokenList.map((token) => makeToken(token)))
|
const tokens = List(erc20Tokens.map((token) => makeToken(token)))
|
||||||
|
|
||||||
dispatch(saveTokens(tokens))
|
dispatch(saveTokens(tokens))
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { makeToken } from 'src/logic/tokens/store/model/token'
|
import { makeToken } from 'src/logic/tokens/store/model/token'
|
||||||
import { getERC20DecimalsAndSymbol, isERC721Contract, isTokenTransfer } from 'src/logic/tokens/utils/tokenHelpers'
|
import { getERC20DecimalsAndSymbol, isERC721Contract, isTokenTransfer } from 'src/logic/tokens/utils/tokenHelpers'
|
||||||
import { getMockedTxServiceModel } from 'src/test/utils/safeHelper'
|
import { getMockedTxServiceModel } from 'src/test/utils/safeHelper'
|
||||||
|
import { fromTokenUnit, toTokenUnit } from 'src/logic/tokens/utils/humanReadableValue'
|
||||||
|
|
||||||
describe('isTokenTransfer', () => {
|
describe('isTokenTransfer', () => {
|
||||||
const safeAddress = '0xdfA693da0D16F5E7E78FdCBeDe8FC6eBEa44f1Cf'
|
const safeAddress = '0xdfA693da0D16F5E7E78FdCBeDe8FC6eBEa44f1Cf'
|
||||||
|
@ -113,7 +114,11 @@ describe('getERC20DecimalsAndSymbol', () => {
|
||||||
const generateBatchRequests = require('src/logic/contracts/generateBatchRequests')
|
const generateBatchRequests = require('src/logic/contracts/generateBatchRequests')
|
||||||
const spyTokenInfos = fetchTokens.getTokenInfos.mockImplementationOnce(() => null)
|
const spyTokenInfos = fetchTokens.getTokenInfos.mockImplementationOnce(() => null)
|
||||||
|
|
||||||
const spyGenerateBatchRequest = generateBatchRequests.default.mockImplementationOnce(() => [decimals, symbol])
|
const spyGenerateBatchRequest = generateBatchRequests.default.mockImplementationOnce(() => [
|
||||||
|
undefined,
|
||||||
|
decimals,
|
||||||
|
symbol,
|
||||||
|
])
|
||||||
|
|
||||||
// when
|
// when
|
||||||
const result = await getERC20DecimalsAndSymbol(tokenAddress)
|
const result = await getERC20DecimalsAndSymbol(tokenAddress)
|
||||||
|
@ -171,4 +176,31 @@ describe('isERC721Contract', () => {
|
||||||
expect(result).toEqual(expectedResult)
|
expect(result).toEqual(expectedResult)
|
||||||
expect(standardContractSpy).toHaveBeenCalled()
|
expect(standardContractSpy).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
it('It should return the right conversion from unit to token', () => {
|
||||||
|
// given
|
||||||
|
const decimals = Number(18)
|
||||||
|
|
||||||
|
const expectedResult = '0.000000003'
|
||||||
|
const ESTIMATED_GAS_COST = 3e9 // 3 Gwei
|
||||||
|
|
||||||
|
// when
|
||||||
|
const gasCosts = fromTokenUnit(ESTIMATED_GAS_COST, decimals)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(gasCosts).toEqual(expectedResult)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('It should return the right conversion from token to unit', () => {
|
||||||
|
// given
|
||||||
|
const decimals = Number(18)
|
||||||
|
|
||||||
|
const expectedResult = '300000000000000000'
|
||||||
|
const VALUE = 0.3
|
||||||
|
|
||||||
|
// when
|
||||||
|
const txValue = toTokenUnit(VALUE, decimals)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(txValue).toEqual(expectedResult)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -3,3 +3,17 @@ import { BigNumber } from 'bignumber.js'
|
||||||
export const humanReadableValue = (value: number | string, decimals = 18): string => {
|
export const humanReadableValue = (value: number | string, decimals = 18): string => {
|
||||||
return new BigNumber(value).times(`1e-${decimals}`).toFixed()
|
return new BigNumber(value).times(`1e-${decimals}`).toFixed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const fromTokenUnit = (amount: number | string, decimals: string | number): string =>
|
||||||
|
new BigNumber(amount).times(`1e-${decimals}`).toFixed()
|
||||||
|
|
||||||
|
export const toTokenUnit = (amount: number | string, decimals: string | number): string => {
|
||||||
|
const amountBN = new BigNumber(amount).times(`1e${decimals}`)
|
||||||
|
const [, amountDecimalPlaces] = amount.toString().split('.')
|
||||||
|
|
||||||
|
if (amountDecimalPlaces?.length >= +decimals) {
|
||||||
|
return amountBN.toFixed(+decimals, BigNumber.ROUND_DOWN)
|
||||||
|
}
|
||||||
|
|
||||||
|
return amountBN.toFixed()
|
||||||
|
}
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import logo from 'src/assets/icons/icon_etherTokens.svg'
|
import { AbiItem } from 'web3-utils'
|
||||||
|
|
||||||
|
import { getNetworkInfo } from 'src/config'
|
||||||
import generateBatchRequests from 'src/logic/contracts/generateBatchRequests'
|
import generateBatchRequests from 'src/logic/contracts/generateBatchRequests'
|
||||||
import {
|
import {
|
||||||
getStandardTokenContract,
|
getStandardTokenContract,
|
||||||
|
@ -6,22 +8,18 @@ import {
|
||||||
getERC721TokenContract,
|
getERC721TokenContract,
|
||||||
} from 'src/logic/tokens/store/actions/fetchTokens'
|
} from 'src/logic/tokens/store/actions/fetchTokens'
|
||||||
import { makeToken, Token } from 'src/logic/tokens/store/model/token'
|
import { makeToken, Token } from 'src/logic/tokens/store/model/token'
|
||||||
|
import { TokenState } from 'src/logic/tokens/store/reducer/tokens'
|
||||||
import { ALTERNATIVE_TOKEN_ABI } from 'src/logic/tokens/utils/alternativeAbi'
|
import { ALTERNATIVE_TOKEN_ABI } from 'src/logic/tokens/utils/alternativeAbi'
|
||||||
import { web3ReadOnly as web3 } from 'src/logic/wallets/getWeb3'
|
import { web3ReadOnly as web3 } from 'src/logic/wallets/getWeb3'
|
||||||
import { isEmptyData } from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers'
|
import { isEmptyData } from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers'
|
||||||
import { TxServiceModel } from 'src/logic/safe/store/actions/transactions/fetchTransactions/loadOutgoingTransactions'
|
import { TxServiceModel } from 'src/logic/safe/store/actions/transactions/fetchTransactions/loadOutgoingTransactions'
|
||||||
import { Map } from 'immutable'
|
|
||||||
|
|
||||||
export const ETH_ADDRESS = '0x000'
|
|
||||||
export const SAFE_TRANSFER_FROM_WITHOUT_DATA_HASH = '42842e0e'
|
export const SAFE_TRANSFER_FROM_WITHOUT_DATA_HASH = '42842e0e'
|
||||||
|
|
||||||
export const getEthAsToken = (balance: string | number): Token => {
|
export const getEthAsToken = (balance: string | number): Token => {
|
||||||
|
const { nativeCoin } = getNetworkInfo()
|
||||||
return makeToken({
|
return makeToken({
|
||||||
address: ETH_ADDRESS,
|
...nativeCoin,
|
||||||
name: 'Ether',
|
|
||||||
symbol: 'ETH',
|
|
||||||
decimals: 18,
|
|
||||||
logoUri: logo,
|
|
||||||
balance,
|
balance,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -44,18 +42,13 @@ export const isTokenTransfer = (tx: TxServiceModel): boolean => {
|
||||||
return !isEmptyData(tx.data) && tx.data?.substring(0, 10) === '0xa9059cbb' && Number(tx.value) === 0
|
return !isEmptyData(tx.data) && tx.data?.substring(0, 10) === '0xa9059cbb' && Number(tx.value) === 0
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isSendERC721Transaction = (
|
export const isSendERC721Transaction = (tx: TxServiceModel, txCode?: string, knownTokens?: TokenState): boolean => {
|
||||||
tx: TxServiceModel,
|
|
||||||
txCode: string | null,
|
|
||||||
knownTokens: Map<string, Token>,
|
|
||||||
): boolean => {
|
|
||||||
// "0x57f1887a8BF19b14fC0dF6Fd9B2acc9Af147eA85" - ens token contract, includes safeTransferFrom
|
// "0x57f1887a8BF19b14fC0dF6Fd9B2acc9Af147eA85" - ens token contract, includes safeTransferFrom
|
||||||
// but no proper ERC721 standard implemented
|
// but no proper ERC721 standard implemented
|
||||||
return (
|
return (
|
||||||
(txCode &&
|
(txCode?.includes(SAFE_TRANSFER_FROM_WITHOUT_DATA_HASH) &&
|
||||||
txCode.includes(SAFE_TRANSFER_FROM_WITHOUT_DATA_HASH) &&
|
|
||||||
tx.to !== '0x57f1887a8BF19b14fC0dF6Fd9B2acc9Af147eA85') ||
|
tx.to !== '0x57f1887a8BF19b14fC0dF6Fd9B2acc9Af147eA85') ||
|
||||||
(isTokenTransfer(tx) && !knownTokens.get(tx.to))
|
(isTokenTransfer(tx) && !knownTokens?.get(tx.to))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,14 +72,16 @@ export const getERC20DecimalsAndSymbol = async (
|
||||||
const storedTokenInfo = await getTokenInfos(tokenAddress)
|
const storedTokenInfo = await getTokenInfos(tokenAddress)
|
||||||
|
|
||||||
if (!storedTokenInfo) {
|
if (!storedTokenInfo) {
|
||||||
const [tokenDecimals, tokenSymbol] = await generateBatchRequests({
|
const [, tokenDecimals, tokenSymbol] = await generateBatchRequests<
|
||||||
abi: ALTERNATIVE_TOKEN_ABI,
|
[undefined, string | undefined, string | undefined]
|
||||||
|
>({
|
||||||
|
abi: ALTERNATIVE_TOKEN_ABI as AbiItem[],
|
||||||
address: tokenAddress,
|
address: tokenAddress,
|
||||||
methods: ['decimals', 'symbol'],
|
methods: ['decimals', 'symbol'],
|
||||||
})
|
})
|
||||||
return { decimals: Number(tokenDecimals), symbol: tokenSymbol }
|
return { decimals: Number(tokenDecimals), symbol: tokenSymbol ?? 'UNKNOWN' }
|
||||||
}
|
}
|
||||||
return { decimals: storedTokenInfo.decimals as number, symbol: storedTokenInfo.symbol }
|
return { decimals: Number(storedTokenInfo.decimals), symbol: storedTokenInfo.symbol }
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`Failed to retrieve token info for ERC20 token ${tokenAddress}`)
|
console.error(`Failed to retrieve token info for ERC20 token ${tokenAddress}`)
|
||||||
}
|
}
|
||||||
|
@ -96,8 +91,8 @@ export const getERC20DecimalsAndSymbol = async (
|
||||||
|
|
||||||
export const isSendERC20Transaction = async (
|
export const isSendERC20Transaction = async (
|
||||||
tx: TxServiceModel,
|
tx: TxServiceModel,
|
||||||
txCode: string | null,
|
txCode?: string,
|
||||||
knownTokens: Map<string, Token>,
|
knownTokens?: TokenState,
|
||||||
): Promise<boolean> => {
|
): Promise<boolean> => {
|
||||||
let isSendTokenTx = !isSendERC721Transaction(tx, txCode, knownTokens) && isTokenTransfer(tx)
|
let isSendTokenTx = !isSendERC721Transaction(tx, txCode, knownTokens) && isTokenTransfer(tx)
|
||||||
|
|
||||||
|
|
|
@ -1,24 +1,12 @@
|
||||||
import Web3 from 'web3'
|
import Web3 from 'web3'
|
||||||
|
import { provider as Provider } from 'web3-core'
|
||||||
|
import { ContentHash } from 'web3-eth-ens'
|
||||||
|
|
||||||
import { sameAddress } from './ethAddresses'
|
import { sameAddress } from './ethAddresses'
|
||||||
import { EMPTY_DATA } from './ethTransactions'
|
import { EMPTY_DATA } from './ethTransactions'
|
||||||
|
|
||||||
import { getNetwork } from '../../config'
|
|
||||||
import { ContentHash } from 'web3-eth-ens'
|
|
||||||
import { provider as Provider } from 'web3-core'
|
|
||||||
import { ProviderProps } from './store/model/provider'
|
import { ProviderProps } from './store/model/provider'
|
||||||
|
import { NODE_ENV } from 'src/utils/constants'
|
||||||
export const ETHEREUM_NETWORK = {
|
import { getRpcServiceUrl } from 'src/config'
|
||||||
MAINNET: 'MAINNET' as const,
|
|
||||||
MORDEN: 'MORDEN' as const,
|
|
||||||
ROPSTEN: 'ROPSTEN' as const,
|
|
||||||
RINKEBY: 'RINKEBY' as const,
|
|
||||||
GOERLI: 'GOERLI' as const,
|
|
||||||
KOVAN: 'KOVAN' as const,
|
|
||||||
UNKNOWN: 'UNKNOWN' as const,
|
|
||||||
}
|
|
||||||
|
|
||||||
export type EthereumNetworks = typeof ETHEREUM_NETWORK[keyof typeof ETHEREUM_NETWORK]
|
|
||||||
|
|
||||||
export const WALLET_PROVIDER = {
|
export const WALLET_PROVIDER = {
|
||||||
SAFE: 'SAFE',
|
SAFE: 'SAFE',
|
||||||
|
@ -38,34 +26,16 @@ export const WALLET_PROVIDER = {
|
||||||
TREZOR: 'TREZOR',
|
TREZOR: 'TREZOR',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ETHEREUM_NETWORK_IDS = {
|
|
||||||
1: ETHEREUM_NETWORK.MAINNET,
|
|
||||||
2: ETHEREUM_NETWORK.MORDEN,
|
|
||||||
3: ETHEREUM_NETWORK.ROPSTEN,
|
|
||||||
4: ETHEREUM_NETWORK.RINKEBY,
|
|
||||||
5: ETHEREUM_NETWORK.GOERLI,
|
|
||||||
42: ETHEREUM_NETWORK.KOVAN,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getEtherScanLink = (type: string, value: string): string => {
|
|
||||||
const network = getNetwork()
|
|
||||||
return `https://${
|
|
||||||
network.toLowerCase() === 'mainnet' ? '' : `${network.toLowerCase()}.`
|
|
||||||
}etherscan.io/${type}/${value}`
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getInfuraUrl = (): string => {
|
|
||||||
const isMainnet = process.env.REACT_APP_NETWORK === 'mainnet'
|
|
||||||
|
|
||||||
return `https://${isMainnet ? 'mainnet' : 'rinkeby'}.infura.io:443/v3/${process.env.REACT_APP_INFURA_TOKEN}`
|
|
||||||
}
|
|
||||||
|
|
||||||
// With some wallets from web3connect you have to use their provider instance only for signing
|
// With some wallets from web3connect you have to use their provider instance only for signing
|
||||||
// And our own one to fetch data
|
// And our own one to fetch data
|
||||||
export const web3ReadOnly =
|
const httpProviderOptions = {
|
||||||
|
timeout: 10_000,
|
||||||
|
}
|
||||||
|
export const web3ReadOnly = new Web3(
|
||||||
process.env.NODE_ENV !== 'test'
|
process.env.NODE_ENV !== 'test'
|
||||||
? new Web3(new Web3.providers.HttpProvider(getInfuraUrl()))
|
? new Web3.providers.HttpProvider(getRpcServiceUrl(), httpProviderOptions)
|
||||||
: new Web3(window.web3?.currentProvider || 'ws://localhost:8545')
|
: window.web3?.currentProvider || 'ws://localhost:8545',
|
||||||
|
)
|
||||||
|
|
||||||
let web3 = web3ReadOnly
|
let web3 = web3ReadOnly
|
||||||
export const getWeb3 = (): Web3 => web3
|
export const getWeb3 = (): Web3 => web3
|
||||||
|
@ -77,7 +47,7 @@ export const resetWeb3 = (): void => {
|
||||||
export const getAccountFrom = async (web3Provider: Web3): Promise<string | null> => {
|
export const getAccountFrom = async (web3Provider: Web3): Promise<string | null> => {
|
||||||
const accounts = await web3Provider.eth.getAccounts()
|
const accounts = await web3Provider.eth.getAccounts()
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'test' && window.testAccountIndex) {
|
if (NODE_ENV === 'test' && window.testAccountIndex) {
|
||||||
return accounts[window.testAccountIndex]
|
return accounts[window.testAccountIndex]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,10 +2,10 @@ import ReactGA from 'react-ga'
|
||||||
|
|
||||||
import addProvider from './addProvider'
|
import addProvider from './addProvider'
|
||||||
|
|
||||||
import { getNetwork } from 'src/config'
|
import { getNetworkId, getNetworkInfo } from 'src/config'
|
||||||
import { NOTIFICATIONS, enhanceSnackbarForAction } from 'src/logic/notifications'
|
import { NOTIFICATIONS, enhanceSnackbarForAction } from 'src/logic/notifications'
|
||||||
import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar'
|
import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar'
|
||||||
import { ETHEREUM_NETWORK, ETHEREUM_NETWORK_IDS, getProviderInfo, getWeb3 } from 'src/logic/wallets/getWeb3'
|
import { getProviderInfo, getWeb3 } from 'src/logic/wallets/getWeb3'
|
||||||
import { makeProvider } from 'src/logic/wallets/store/model/provider'
|
import { makeProvider } from 'src/logic/wallets/store/model/provider'
|
||||||
import { updateStoredTransactionsStatus } from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers'
|
import { updateStoredTransactionsStatus } from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers'
|
||||||
import { Dispatch } from 'redux'
|
import { Dispatch } from 'redux'
|
||||||
|
@ -24,12 +24,13 @@ const handleProviderNotification = (provider, dispatch) => {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ETHEREUM_NETWORK_IDS[network] !== getNetwork()) {
|
if (network !== getNetworkId()) {
|
||||||
dispatch(enqueueSnackbar(NOTIFICATIONS.WRONG_NETWORK_MSG))
|
dispatch(enqueueSnackbar(NOTIFICATIONS.WRONG_NETWORK_MSG))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (ETHEREUM_NETWORK.RINKEBY === getNetwork()) {
|
|
||||||
dispatch(enqueueSnackbar(enhanceSnackbarForAction(NOTIFICATIONS.RINKEBY_VERSION_MSG)))
|
if (getNetworkInfo().isTestNet) {
|
||||||
|
dispatch(enqueueSnackbar(enhanceSnackbarForAction(NOTIFICATIONS.TESTNET_VERSION_MSG)))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (available) {
|
if (available) {
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
import { Record, RecordOf } from 'immutable'
|
import { Record, RecordOf } from 'immutable'
|
||||||
|
|
||||||
|
import { ETHEREUM_NETWORK } from 'src/config/networks/network.d'
|
||||||
|
|
||||||
export type ProviderProps = {
|
export type ProviderProps = {
|
||||||
name: string
|
name: string
|
||||||
loaded: boolean
|
loaded: boolean
|
||||||
available: boolean
|
available: boolean
|
||||||
account: string
|
account: string
|
||||||
network: number
|
network: ETHEREUM_NETWORK
|
||||||
smartContractWallet: boolean
|
smartContractWallet: boolean
|
||||||
hardwareWallet: boolean
|
hardwareWallet: boolean
|
||||||
}
|
}
|
||||||
|
@ -15,7 +17,7 @@ export const makeProvider = Record<ProviderProps>({
|
||||||
loaded: false,
|
loaded: false,
|
||||||
available: false,
|
available: false,
|
||||||
account: '',
|
account: '',
|
||||||
network: 0,
|
network: ETHEREUM_NETWORK.UNKNOWN,
|
||||||
smartContractWallet: false,
|
smartContractWallet: false,
|
||||||
hardwareWallet: false,
|
hardwareWallet: false,
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { createSelector } from 'reselect'
|
import { createSelector } from 'reselect'
|
||||||
|
|
||||||
import { ETHEREUM_NETWORK, ETHEREUM_NETWORK_IDS, EthereumNetworks } from 'src/logic/wallets/getWeb3'
|
import { ETHEREUM_NETWORK } from 'src/config/networks/network.d'
|
||||||
import { PROVIDER_REDUCER_ID, ProviderState } from 'src/logic/wallets/store/reducer/provider'
|
import { PROVIDER_REDUCER_ID, ProviderState } from 'src/logic/wallets/store/reducer/provider'
|
||||||
import { AppReduxState } from 'src/store'
|
import { AppReduxState } from 'src/store'
|
||||||
|
|
||||||
|
@ -18,9 +18,10 @@ export const providerNameSelector = createSelector(providerSelector, (provider:
|
||||||
|
|
||||||
export const networkSelector = createSelector(
|
export const networkSelector = createSelector(
|
||||||
providerSelector,
|
providerSelector,
|
||||||
(provider: ProviderState): EthereumNetworks => {
|
(provider: ProviderState): ETHEREUM_NETWORK => {
|
||||||
const networkId = provider.get('network')
|
const networkId = provider.get('network')
|
||||||
return ETHEREUM_NETWORK_IDS[networkId] || ETHEREUM_NETWORK.UNKNOWN
|
|
||||||
|
return networkId ?? ETHEREUM_NETWORK.UNKNOWN
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,19 +1,25 @@
|
||||||
import { getInfuraUrl } from '../getWeb3'
|
import { WalletInitOptions } from 'bnc-onboard/dist/src/interfaces'
|
||||||
|
|
||||||
const isMainnet = process.env.REACT_APP_NETWORK === 'mainnet'
|
import { getNetworkId, getRpcServiceUrl } from 'src/config'
|
||||||
|
import { ETHEREUM_NETWORK } from 'src/config/networks/network.d'
|
||||||
|
import { FORTMATIC_KEY, PORTIS_ID } from 'src/utils/constants'
|
||||||
|
|
||||||
const PORTIS_DAPP_ID = isMainnet ? process.env.REACT_APP_PORTIS_ID : '852b763d-f28b-4463-80cb-846d7ec5806b'
|
const networkId = getNetworkId()
|
||||||
// const SQUARELINK_CLIENT_ID = isMainnet ? process.env.REACT_APP_SQUARELINK_ID : '46ce08fe50913cfa1b78'
|
const PORTIS_DAPP_ID = PORTIS_ID[networkId] ?? PORTIS_ID[ETHEREUM_NETWORK.RINKEBY]
|
||||||
const FORTMATIC_API_KEY = isMainnet ? process.env.REACT_APP_FORTMATIC_KEY : 'pk_test_CAD437AA29BE0A40'
|
const FORTMATIC_API_KEY = FORTMATIC_KEY[networkId] ?? FORTMATIC_KEY[ETHEREUM_NETWORK.RINKEBY]
|
||||||
|
|
||||||
const infuraUrl = getInfuraUrl()
|
type Wallet = WalletInitOptions & {
|
||||||
|
desktop: boolean
|
||||||
|
}
|
||||||
|
|
||||||
const wallets = [
|
const rpcUrl = getRpcServiceUrl()
|
||||||
|
const wallets: Wallet[] = [
|
||||||
{ walletName: 'metamask', preferred: true, desktop: false },
|
{ walletName: 'metamask', preferred: true, desktop: false },
|
||||||
{
|
{
|
||||||
walletName: 'walletConnect',
|
walletName: 'walletConnect',
|
||||||
preferred: true,
|
preferred: true,
|
||||||
infuraKey: process.env.REACT_APP_INFURA_TOKEN,
|
// as stated in the documentation, `infuraKey` is not mandatory if rpc is provided
|
||||||
|
rpc: { [networkId]: rpcUrl },
|
||||||
desktop: true,
|
desktop: true,
|
||||||
bridge: 'https://safe-walletconnect.gnosis.io/',
|
bridge: 'https://safe-walletconnect.gnosis.io/',
|
||||||
},
|
},
|
||||||
|
@ -23,13 +29,13 @@ const wallets = [
|
||||||
preferred: true,
|
preferred: true,
|
||||||
email: 'safe@gnosis.io',
|
email: 'safe@gnosis.io',
|
||||||
desktop: true,
|
desktop: true,
|
||||||
rpcUrl: infuraUrl,
|
rpcUrl,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
walletName: 'ledger',
|
walletName: 'ledger',
|
||||||
desktop: true,
|
desktop: true,
|
||||||
preferred: true,
|
preferred: true,
|
||||||
rpcUrl: infuraUrl,
|
rpcUrl,
|
||||||
LedgerTransport: (window as any).TransportNodeHid,
|
LedgerTransport: (window as any).TransportNodeHid,
|
||||||
},
|
},
|
||||||
{ walletName: 'trust', preferred: true, desktop: false },
|
{ walletName: 'trust', preferred: true, desktop: false },
|
||||||
|
@ -48,16 +54,18 @@ const wallets = [
|
||||||
{ walletName: 'torus', desktop: true },
|
{ walletName: 'torus', desktop: true },
|
||||||
{ walletName: 'unilogin', desktop: true },
|
{ walletName: 'unilogin', desktop: true },
|
||||||
{ walletName: 'coinbase', desktop: false },
|
{ walletName: 'coinbase', desktop: false },
|
||||||
{ walletName: 'walletLink', rpcUrl: infuraUrl, desktop: false },
|
{ walletName: 'walletLink', rpcUrl, desktop: false },
|
||||||
{ walletName: 'opera', desktop: false },
|
{ walletName: 'opera', desktop: false },
|
||||||
{ walletName: 'operaTouch', desktop: false },
|
{ walletName: 'operaTouch', desktop: false },
|
||||||
]
|
]
|
||||||
|
|
||||||
export const getSupportedWallets = () => {
|
export const getSupportedWallets = (): WalletInitOptions[] => {
|
||||||
const { isDesktop } = window as any
|
const { isDesktop } = window as any
|
||||||
/* eslint-disable no-unused-vars */
|
/* eslint-disable no-unused-vars */
|
||||||
|
|
||||||
if (isDesktop) return wallets.filter((wallet) => wallet.desktop).map(({ desktop, ...rest }) => rest)
|
if (isDesktop) {
|
||||||
|
return wallets.filter((wallet) => wallet.desktop).map(({ desktop, ...rest }) => rest)
|
||||||
|
}
|
||||||
|
|
||||||
return wallets.map(({ desktop, ...rest }) => rest)
|
return wallets.map(({ desktop, ...rest }) => rest)
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,7 +43,7 @@ const useStyles = makeStyles({
|
||||||
})
|
})
|
||||||
|
|
||||||
export const SAFE_INSTANCE_ERROR = 'Address given is not a Safe instance'
|
export const SAFE_INSTANCE_ERROR = 'Address given is not a Safe instance'
|
||||||
export const SAFE_MASTERCOPY_ERROR = 'Mastercopy used by this Safe is not the same'
|
export const SAFE_MASTERCOPY_ERROR = 'Address is not a Safe or mastercopy is not supported'
|
||||||
|
|
||||||
// In case of an error here, it will be swallowed by final-form
|
// In case of an error here, it will be swallowed by final-form
|
||||||
// So if you're experiencing any strang behaviours like freeze or hanging
|
// So if you're experiencing any strang behaviours like freeze or hanging
|
||||||
|
@ -73,7 +73,7 @@ export const safeFieldsValidation = async (values): Promise<Record<string, strin
|
||||||
`0x${proxyAddressFromStorage.substr(proxyAddressFromStorage.length - 40)}`,
|
`0x${proxyAddressFromStorage.substr(proxyAddressFromStorage.length - 40)}`,
|
||||||
)
|
)
|
||||||
const safeMaster = await getSafeMasterContract()
|
const safeMaster = await getSafeMasterContract()
|
||||||
const masterCopy = safeMaster.address
|
const masterCopy = safeMaster.options.address
|
||||||
const sameMasterCopy =
|
const sameMasterCopy =
|
||||||
checksummedProxyAddress === masterCopy || checksummedProxyAddress === SAFE_MASTER_COPY_ADDRESS_V10
|
checksummedProxyAddress === masterCopy || checksummedProxyAddress === SAFE_MASTER_COPY_ADDRESS_V10
|
||||||
if (!sameMasterCopy) {
|
if (!sameMasterCopy) {
|
||||||
|
|
|
@ -1,28 +1,28 @@
|
||||||
import TableContainer from '@material-ui/core/TableContainer'
|
|
||||||
import { makeStyles } from '@material-ui/core/styles'
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
|
import TableContainer from '@material-ui/core/TableContainer'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
|
||||||
import CopyBtn from 'src/components/CopyBtn'
|
import CopyBtn from 'src/components/CopyBtn'
|
||||||
import EtherscanBtn from 'src/components/EtherscanBtn'
|
import EtherscanBtn from 'src/components/EtherscanBtn'
|
||||||
import Identicon from 'src/components/Identicon'
|
|
||||||
import OpenPaper from 'src/components/Stepper/OpenPaper'
|
|
||||||
import Field from 'src/components/forms/Field'
|
import Field from 'src/components/forms/Field'
|
||||||
import TextField from 'src/components/forms/TextField'
|
import TextField from 'src/components/forms/TextField'
|
||||||
import { composeValidators, minMaxLength, required } from 'src/components/forms/validator'
|
import { composeValidators, minMaxLength, required } from 'src/components/forms/validator'
|
||||||
|
import Identicon from 'src/components/Identicon'
|
||||||
import Block from 'src/components/layout/Block'
|
import Block from 'src/components/layout/Block'
|
||||||
import Col from 'src/components/layout/Col'
|
import Col from 'src/components/layout/Col'
|
||||||
import Hairline from 'src/components/layout/Hairline'
|
import Hairline from 'src/components/layout/Hairline'
|
||||||
import Paragraph from 'src/components/layout/Paragraph'
|
import Paragraph from 'src/components/layout/Paragraph'
|
||||||
import Row from 'src/components/layout/Row'
|
import Row from 'src/components/layout/Row'
|
||||||
import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
|
import OpenPaper from 'src/components/Stepper/OpenPaper'
|
||||||
import { FIELD_LOAD_ADDRESS, THRESHOLD } from 'src/routes/load/components/fields'
|
import { AddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
||||||
import { getOwnerAddressBy, getOwnerNameBy } from 'src/routes/open/components/fields'
|
|
||||||
|
|
||||||
import { useSelector } from 'react-redux'
|
|
||||||
import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
|
import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
|
||||||
|
|
||||||
import { formatAddressListToAddressBookNames } from 'src/logic/addressBook/utils'
|
import { formatAddressListToAddressBookNames } from 'src/logic/addressBook/utils'
|
||||||
import { AddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
|
||||||
|
import { FIELD_LOAD_ADDRESS, THRESHOLD } from 'src/routes/load/components/fields'
|
||||||
|
import { getOwnerAddressBy, getOwnerNameBy } from 'src/routes/open/components/fields'
|
||||||
import { styles } from './styles'
|
import { styles } from './styles'
|
||||||
|
|
||||||
const calculateSafeValues = (owners, threshold, values) => {
|
const calculateSafeValues = (owners, threshold, values) => {
|
||||||
|
@ -112,7 +112,7 @@ const OwnerListComponent = (props) => {
|
||||||
{address}
|
{address}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<CopyBtn content={address} />
|
<CopyBtn content={address} />
|
||||||
<EtherscanBtn type="address" value={address} />
|
<EtherscanBtn value={address} />
|
||||||
</Row>
|
</Row>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
|
@ -5,12 +5,12 @@ import React from 'react'
|
||||||
import CopyBtn from 'src/components/CopyBtn'
|
import CopyBtn from 'src/components/CopyBtn'
|
||||||
import EtherscanBtn from 'src/components/EtherscanBtn'
|
import EtherscanBtn from 'src/components/EtherscanBtn'
|
||||||
import Identicon from 'src/components/Identicon'
|
import Identicon from 'src/components/Identicon'
|
||||||
import OpenPaper from 'src/components/Stepper/OpenPaper'
|
|
||||||
import Block from 'src/components/layout/Block'
|
import Block from 'src/components/layout/Block'
|
||||||
import Col from 'src/components/layout/Col'
|
import Col from 'src/components/layout/Col'
|
||||||
import Hairline from 'src/components/layout/Hairline'
|
import Hairline from 'src/components/layout/Hairline'
|
||||||
import Paragraph from 'src/components/layout/Paragraph'
|
import Paragraph from 'src/components/layout/Paragraph'
|
||||||
import Row from 'src/components/layout/Row'
|
import Row from 'src/components/layout/Row'
|
||||||
|
import OpenPaper from 'src/components/Stepper/OpenPaper'
|
||||||
import { shortVersionOf } from 'src/logic/wallets/ethAddresses'
|
import { shortVersionOf } from 'src/logic/wallets/ethAddresses'
|
||||||
import { FIELD_LOAD_ADDRESS, FIELD_LOAD_NAME, THRESHOLD } from 'src/routes/load/components/fields'
|
import { FIELD_LOAD_ADDRESS, FIELD_LOAD_NAME, THRESHOLD } from 'src/routes/load/components/fields'
|
||||||
import { getNumOwnersFrom, getOwnerAddressBy, getOwnerNameBy } from 'src/routes/open/components/fields'
|
import { getNumOwnersFrom, getOwnerAddressBy, getOwnerNameBy } from 'src/routes/open/components/fields'
|
||||||
|
@ -76,7 +76,7 @@ const ReviewComponent = ({ userAddress, values }: Props): React.ReactElement =>
|
||||||
{shortVersionOf(safeAddress, 4)}
|
{shortVersionOf(safeAddress, 4)}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<CopyBtn content={safeAddress} />
|
<CopyBtn content={safeAddress} />
|
||||||
<EtherscanBtn type="address" value={safeAddress} />
|
<EtherscanBtn value={safeAddress} />
|
||||||
</Row>
|
</Row>
|
||||||
</Block>
|
</Block>
|
||||||
<Block margin="lg">
|
<Block margin="lg">
|
||||||
|
@ -121,7 +121,7 @@ const ReviewComponent = ({ userAddress, values }: Props): React.ReactElement =>
|
||||||
{address}
|
{address}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<CopyBtn content={address} />
|
<CopyBtn content={address} />
|
||||||
<EtherscanBtn type="address" value={address} />
|
<EtherscanBtn value={address} />
|
||||||
</Block>
|
</Block>
|
||||||
</Block>
|
</Block>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue