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

This commit is contained in:
Mati Dastugue 2020-04-30 10:34:04 -03:00
commit 041e97c19c
52 changed files with 4629 additions and 459 deletions

View File

@ -27,5 +27,6 @@ REACT_APP_APP_VERSION=$npm_package_version
# all environments
REACT_APP_INFURA_TOKEN=
# For Apps
REACT_APP_GNOSIS_APPS_URL=http://localhost:3002
# For Apps
REACT_APP_GNOSIS_APPS_URL=https://safe-apps.staging.gnosisdev.com
REACT_APP_APPS_DISABLED=false

View File

@ -0,0 +1,55 @@
name: Build/release
# this will help you specify where to run
on:
push:
branches:
# this will run on the specified branch
- feature/desktop-app
env:
REACT_APP_BLOCKNATIVE_KEY: ${{ secrets.REACT_APP_BLOCKNATIVE_KEY }}
REACT_APP_FORTMATIC_KEY: ${{ secrets.REACT_APP_FORTMATIC_KEY }}
REACT_APP_GOOGLE_ANALYTICS_ID_MAINNET: ${{ secrets.REACT_APP_GOOGLE_ANALYTICS_ID_MAINNET }}
REACT_APP_INFURA_TOKEN: ${{ secrets.REACT_APP_INFURA_TOKEN }}
REACT_APP_PORTIS_ID: ${{ secrets.REACT_APP_PORTIS_ID }}
jobs:
release:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [macos-latest, ubuntu-latest]
steps:
- name: Check out Git repository
uses: actions/checkout@v1
- name: Install Node.js, NPM and Yarn
uses: actions/setup-node@v1
with:
node-version: 10.16
- name: Build/release Electron app
env:
# macOS notarization API key
APPLEID: ${{ secrets.APPLE_ID }}
APPLEIDPASS: ${{ secrets.APPLE_ID_PASS }}
uses: samuelmeuli/action-electron-builder@v1
with:
#Build scipt
build_script_name: build-desktop
# GitHub token, automatically provided to the action
# (No need to define this secret in the repo settings)
github_token: ${{ secrets.github_token }}
# macOS code signing certificate
mac_certs: ${{ secrets.MAC_CERTS }}
mac_certs_password: ${{ secrets.MAC_CERTS_PASSWORD }}
# If the commit is tagged with a version (e.g. "v1.0.0"),
# release the app after building
release: ${{ startsWith(github.ref, 'refs/tags/v') }}

View File

@ -1,11 +1,11 @@
name: Build/release
name: Build/Release Desktop app
# this will help you specify where to run
on:
push:
branches:
# this will run on the specified branch
- feature/desktop-app
- master
env:
REACT_APP_BLOCKNATIVE_KEY: ${{ secrets.REACT_APP_BLOCKNATIVE_KEY }}
@ -13,25 +13,42 @@ env:
REACT_APP_GOOGLE_ANALYTICS_ID_MAINNET: ${{ secrets.REACT_APP_GOOGLE_ANALYTICS_ID_MAINNET }}
REACT_APP_INFURA_TOKEN: ${{ secrets.REACT_APP_INFURA_TOKEN }}
REACT_APP_PORTIS_ID: ${{ secrets.REACT_APP_PORTIS_ID }}
REACT_APP_GNOSIS_APPS_URL: ${{ secrets.REACT_APP_GNOSIS_APPS_URL }}
jobs:
release:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
max-parallel: 15
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
os: [macos-latest, windows-latest, ubuntu-latest]
steps:
- name: Check out Git repository
uses: actions/checkout@v1
uses: actions/checkout@v2
- name: Patch node gyp on windows to support Visual Studio 2019
if: startsWith(matrix.os, 'windows')
shell: powershell
run: |
yarn global add --production windows-build-tools --vs2015 --msvs_version=2015
- name: Install node-gyp
if: startsWith(matrix.os, 'windows')
shell: powershell
run: |
yarn global add node-gyp
yarn config set node_gyp "$_\node_modules\node-gyp\bin\node-gyp.js"
- name: Install Node.js, NPM and Yarn
uses: actions/setup-node@v1
with:
node-version: 10.16
- run: yarn install --network-concurrency 1
- name: Build/release Electron app
- name: Build/Release Desktop App
env:
# macOS notarization API key
APPLEID: ${{ secrets.APPLE_ID }}

6
.gitignore vendored
View File

@ -1,9 +1,11 @@
node_modules/
build_webpack/
.DS_Store
build/
./build
yarn-error.log
.env*
.idea/
dist
electron-builder.yml
.yalc/
yalc.lock
yalc.lock

View File

@ -16,9 +16,11 @@ matrix:
- env:
- REACT_APP_NETWORK='mainnet'
- STAGING_BUCKET_NAME=${STAGING_MAINNET_BUCKET_NAME}
- REACT_APP_GNOSIS_APPS_URL=${REACT_APP_GNOSIS_APPS_URL_PROD}
if: (branch = master AND NOT type = pull_request) OR tag IS present
- env:
- REACT_APP_NETWORK='rinkeby'
- REACT_APP_GNOSIS_APPS_URL=${REACT_APP_GNOSIS_APPS_URL_STAGING}
before_install:
# Needed to deploy pull request and releases
- sudo apt-get update

View File

@ -52,9 +52,11 @@ function ensureSlash(path, needsSlash) {
// single-page apps that may serve index.html for nested URLs like /todos/42.
// We can't use a relative path in HTML because we don't want to load something
// like /todos/42/static/js/bundle.7289d.js. We have to know the root.
const buildDesktop = process.env.BUILD_FOR_DESKTOP
const homepagePath = require(paths.appPackageJson).homepage
// var homepagePathname = homepagePath ? url.parse(homepagePath).pathname : '/';
const homepagePathname = "/app/"
const homepagePathname = buildDesktop === 'true' ? "./" : "/app/"
// Webpack uses `publicPath` to determine where the app is being served from.
// It requires a trailing slash, or the file assets will get an incorrect path.
const publicPath = ensureSlash(homepagePathname, true)

View File

@ -1,6 +1,6 @@
{
"name": "safe-react",
"version": "1.9.5",
"version": "2.0.0",
"description": "Allowing crypto users manage funds in a safer way",
"homepage": "https://github.com/gnosis/safe-react#readme",
"bugs": {
@ -8,16 +8,24 @@
},
"repository": {
"type": "git",
"url": "https://github.com/gnosis/safe-react"
"url": "https://github.com/gnosis/safe-react.git"
},
"license": "MIT",
"author": "Gnosis Team",
"directories": {
"test": "test"
"author": {
"name": "Gnosis Team",
"email": "safe@gnosis.io"
},
"main": "public/electron.js",
"postinstall": "electron-builder install-app-deps",
"scripts": {
"build": "REACT_APP_APP_VERSION=$npm_package_version node scripts/build.js",
"build-mainnet": "REACT_APP_NETWORK=mainnet yarn build",
"build": "cross-env REACT_APP_APP_VERSION=$npm_package_version node scripts/build.js",
"electron-build": "electron-builder --mac --windows --linux",
"postinstall": "electron-builder install-app-deps",
"release": "electron-builder --mac --linux --windows -p always",
"electron-dev": "concurrently \"BROWSER=none yarn start\" \"wait-on http://localhost:3000 && electron .\"",
"preelectron-pack": "yarn build",
"build-mainnet": "cross-env REACT_APP_NETWORK=mainnet yarn build",
"build-desktop": "cross-env BUILD_FOR_DESKTOP=true yarn build-mainnet",
"flow": "flow",
"format:staged": "lint-staged",
"lint:check": "eslint './src/**/*.{js,jsx}'",
@ -40,10 +48,97 @@
"prettier --write"
]
},
"productName": "Safe Electron",
"build": {
"appId": "io.gnosis.safe.macos",
"afterSign": "scripts/notarize.js",
"productName": "Safe Electron",
"asar": true,
"publish": [
{
"provider": "github",
"owner": "gnosis",
"repo": "safe-react"
}
],
"dmg": {
"sign": false,
"contents": [
{
"x": 110,
"y": 150
},
{
"x": 240,
"y": 150,
"type": "link",
"path": "/Applications"
}
]
},
"files": [
"**/*",
"!src${/*}",
"!config${/*}",
"!contracts${/*}",
"!migrations${/*}",
"!flow-typed${/*}",
"!apps${/*}",
"!build${/*}",
"!out${/*}",
"!.editorconfig",
"!.gitignore",
"!README.md",
"!yarn-error.log",
"!yarn.lock"
],
"directories": {
"buildResources": "public/build"
},
"mac": {
"category": "public.app-category.productivity",
"hardenedRuntime": true,
"entitlements": "public/build/entitlements.mac.plist",
"gatekeeperAssess": false,
"entitlementsInherit": "public/build/entitlements.mac.plist",
"target": [
"dmg",
"zip"
],
"publish": [
{
"provider": "github",
"owner": "gnosis",
"repo": "safe-react"
}
]
},
"nsis": {
"deleteAppDataOnUninstall": true
},
"linux": {
"target": [
"AppImage",
"deb",
"zip"
],
"icon": "./public/build/safe.png"
},
"win": {
"target": [
"nsis"
],
"icon": "public/build/icon.ico"
}
},
"resolutions": {
"node-gyp": "^5.1.0"
},
"dependencies": {
"@gnosis.pm/safe-contracts": "1.1.1-dev.1",
"@gnosis.pm/util-contracts": "2.0.6",
"@gnosis.pm/safe-react-components": "https://github.com/gnosis/safe-react-components.git#71e6fed",
"@gnosis.pm/safe-contracts": "1.1.1-dev.2",
"@gnosis.pm/util-contracts": "2.0.6",
"@gnosis.pm/safe-react-components": "https://github.com/gnosis/safe-react-components.git#a057248",
"@ledgerhq/hw-transport-node-hid": "5.12.0",
"@material-ui/core": "4.9.10",
"@material-ui/icons": "4.9.1",
"@material-ui/lab": "4.0.0-alpha.39",
@ -55,18 +150,26 @@
"bignumber.js": "9.0.0",
"bnc-onboard": "1.7.6",
"connected-react-router": "6.8.0",
"cross-env": "^7.0.2",
"currency-flags": "^2.1.1",
"date-fns": "2.12.0",
"dotenv": "^8.2.0",
"electron-is-dev": "^1.1.0",
"electron-log": "^4.1.1",
"electron-updater": "4.2.0",
"ethereum-ens": "0.8.0",
"express": "^4.17.1",
"final-form": "4.19.1",
"history": "4.10.1",
"immortal-db": "^1.0.2",
"immutable": "^4.0.0-rc.9",
"install": "^0.13.0",
"js-cookie": "^2.2.1",
"lint-staged": "10.1.3",
"material-ui-search-bar": "^1.0.0-beta.13",
"notistack": "https://github.com/gnosis/notistack.git#v0.9.4",
"npm": "^6.14.4",
"open": "^7.0.3",
"optimize-css-assets-webpack-plugin": "5.0.3",
"polished": "3.5.1",
"qrcode.react": "1.0.0",
@ -89,6 +192,7 @@
"reselect": "^4.0.0",
"semver": "7.3.2",
"styled-components": "^5.0.1",
"wait-on": "^4.0.1",
"web3": "1.2.6"
},
"devDependencies": {
@ -127,9 +231,13 @@
"babel-plugin-transform-es3-property-literals": "^6.22.0",
"babel-polyfill": "^6.26.0",
"classnames": "^2.2.6",
"concurrently": "4.1.2",
"css-loader": "3.5.2",
"detect-port": "^1.3.0",
"dotenv-expand": "^5.1.0",
"electron": "7.1.8",
"electron-builder": "22.2.0",
"electron-notarize": "^0.2.1",
"eslint": "^6.8.0",
"eslint-config-prettier": "6.10.1",
"eslint-plugin-flowtype": "4.7.0",

View File

@ -0,0 +1,69 @@
const os = require('os');
const fetch = require('node-fetch');
const { dialog, app } = require('electron');
const log = require('electron-log');
const isDev = require("electron-is-dev");
const { autoUpdater } = require("electron-updater");
// This logging setup is not required for auto-updates to work,
// but it sure makes debugging easier :)
//-------------------------------------------------------------------
autoUpdater.autoDownload = false
autoUpdater.logger = log;
autoUpdater.logger.transports.file.level = 'info';
log.info('App starting...');
let initialized = false;
let downloadProgress = 0;
function init(mainWindow) {
if(initialized || isDev) return;
initialized = true;
autoUpdater.on('error', (error) => {
dialog.showErrorBox('Error: ', error == null ? "unknown" : (error.stack || error).toString());
});
autoUpdater.on('update-available', () => {
dialog.showMessageBox({
type: 'info',
title: 'Found Updates',
message: 'There is a newer version of this app available. Do you want to update now?',
buttons: ['Yes', 'Remind me later'],
cancelId:1,
}).then(result => {
if(result.response === 0){
autoUpdater.downloadUpdate();
}
});
autoUpdater.on('update-downloaded', (event, releaseNotes, releaseName) => {
autoUpdater.logger.info("Update Downloaded...");
dialog.showMessageBox({
title: 'Install Updates',
message: process.platform === 'win32' ? releaseNotes : releaseName,
detail: 'A new version has been downloaded. Restart the application to apply the updates.',
buttons: ['Restart', 'Cancel'],
cancelId:1,
}).then(result => {
if(result.response === 0){
autoUpdater.quitAndInstall();
}
});
});
});
autoUpdater.on("download-progress", (d) => {
downloadProgress = d.percent;
autoUpdater.logger.info(downloadProgress);
});
autoUpdater.checkForUpdates();
}
module.exports = {
init,
};

BIN
public/build/all-certs.p12 Normal file

Binary file not shown.

BIN
public/build/background.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.device.camera</key>
<true/>
<key>com.apple.security.device.microphone</key>
<true/>
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
</dict>
</plist>

BIN
public/build/icon.icns Normal file

Binary file not shown.

BIN
public/build/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

BIN
public/build/safe.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

162
public/electron.js Normal file
View File

@ -0,0 +1,162 @@
const electron = require("electron");
const express = require('express');
const open = require('open');
const log = require('electron-log');
const fs = require('fs');
const dialog = electron.dialog;
const Menu = electron.Menu;
const https = require('https');
const autoUpdater = require('./auto-updater');
const url = require('url');
const app = electron.app;
const session = electron.session;
const BrowserWindow = electron.BrowserWindow;
const path = require("path");
const isDev = require("electron-is-dev");
const options = {
key: fs.readFileSync(path.join(__dirname, './ssl/server.key')),
cert: fs.readFileSync(path.join(__dirname, './ssl/server.crt')),
ca: fs.readFileSync(path.join(__dirname, './ssl/rootCA.crt'))
};
const PORT = 5000;
const createServer = () => {
const app = express();
const staticRoute = path.join(__dirname, '../build_webpack');
app.use(express.static(staticRoute));
https.createServer(options, app).listen(PORT);
}
let mainWindow;
function getOpenedWindow(url,options) {
let display = electron.screen.getPrimaryDisplay();
let width = display.bounds.width;
let height = display.bounds.height;
// filter all requests to trezor-bridge and change origin to make it work
const filter = {
urls: ['http://127.0.0.1:21325/*']
};
options.webPreferences.affinity = 'main-window';
if(url.includes('about:blank')){
/*
session.defaultSession.webRequest.onBeforeSendHeaders(filter, (details, callback) => {
details.requestHeaders['Origin'] = 'https://electron.trezor.io';
callback({cancel: false, requestHeaders: details.requestHeaders});
});
*/
}
if(url.includes('wallet.portis') || url.includes('about:blank') || url.includes('app.tor.us')){
const win = new BrowserWindow({
width:300,
height:700,
x: width - 1300,
parent:mainWindow,
y: height - 200,
webContents: options.webContents, // use existing webContents if provided
fullscreen: false,
show: false,
});
win.once('ready-to-show', () => win.show());
if(!options.webPreferences){
win.loadURL(url);
}
return win
}
return null;
}
function createWindow() {
mainWindow = new BrowserWindow({
show: false,
width: 1024,
height: 768,
webPreferences: {
preload: path.join(__dirname, '../scripts/preload.js'),
allowRunningInsecureContent: true,
nativeWindowOpen: true, // need to be set in order to display modal
},
icon: path.join(__dirname, './build/safe.png'),
});
mainWindow.once('ready-to-show', () => {
mainWindow.show();
});
mainWindow.loadURL(
isDev
? "http://localhost:3000"
: `https://localhost:${PORT}`
)
if (isDev) {
// Open the DevTools.
mainWindow.webContents.openDevTools();
//BrowserWindow.addDevToolsExtension('<location to your react chrome extension>');
}
mainWindow.setMenu(null);
mainWindow.setMenuBarVisibility(false);
mainWindow.webContents.on('new-window', function(event, url, frameName, disposition, options){
event.preventDefault();
const win = getOpenedWindow(url,options);
if(win){
win.once('ready-to-show', () => win.show());
if(!options.webPreferences){
win.loadURL(url);
}
event.newGuest = win
} else open(url);
});
mainWindow.webContents.on('did-finish-load', () => {
autoUpdater.init(mainWindow);
});
mainWindow.webContents.on('crashed', () => {
log.info('App Crashed');
mainWindow.reload();
});
mainWindow.on("closed", () => (mainWindow = null));
}
app.userAgentFallback = process.platform ==='win32' ?
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.100 Safari/537.36' :
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) old-airport-include/1.0.0 Chrome Electron/7.1.7 Safari/537.36';
app.commandLine.appendSwitch('ignore-certificate-errors');
app.on("ready", () =>{
// Hide the menu
//Menu.setApplicationMenu(null);
if(!isDev) createServer();
createWindow();
});
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
app.quit();
}
});
app.on("activate", () => {
if (mainWindow === null) {
createWindow();
}
});

20
public/ssl/client.crt Normal file
View File

@ -0,0 +1,20 @@
-----BEGIN CERTIFICATE-----
MIIDTzCCAjcCFA3n/7f/k+b9g/7W6zodg+u2qC3BMA0GCSqGSIb3DQEBCwUAMGQx
CzAJBgNVBAYTAkRFMRAwDgYDVQQIDAdHZXJtYW55MREwDwYDVQQHDAhXYWxsZG9y
ZjEPMA0GA1UECgwGU0FQIFNFMQ4wDAYDVQQLDAVUb29sczEPMA0GA1UEAwwGcm9v
dENBMB4XDTIwMDMyNzIxMTkxNFoXDTIxMDgwOTIxMTkxNFowZDELMAkGA1UEBhMC
REUxEDAOBgNVBAgMB0dlcm1hbnkxETAPBgNVBAcMCFdhbGxkb3JmMQ8wDQYDVQQK
DAZTQVAgU0UxDjAMBgNVBAsMBVRvb2xzMQ8wDQYDVQQDDAZjbGllbnQwggEiMA0G
CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCZ7hsC/0FOfOUUyefqhFvmiYJoqMSi
8/vvjJKn+TCXhlfX/BLxBtJN8BlFNDux+qPlPhElbCg0bldeBGkZNgD7Jt6Fjpkd
SqqoDIcfl+oxAks76Qi5hh2FKHSOp3BmHgVuur+cbzOd8J+NsskGYay32mAHLrq8
ixPLUtkOO9W2PSKm9KQEwOdYV9R/dStvZDA5dEVEDGv3MIBgRVzyu8gGwMfjzci1
wgwU5Eb2r2b7Vs19nAoLQwelBf4bL5Z5b2KjfW1HPhmtM1eBaf+3bMscnemAgY8I
0ZHMS0XjORLvSBKZ73Q1K9lv6dc45fQA6g3KnVvFSB0nfqbhw7vuDEXrAgMBAAEw
DQYJKoZIhvcNAQELBQADggEBAAzKry8DXN6tlIE5ZRp9z/MdT8bOSwNQM9H/E1Rn
50fP5C3m5IZioYdsfQtDvEC2bHHIYyWvqL6AAWVOzA8Pvnw1J32Sq3Tz5EwH0B5p
wRVxB2GEe7WqSQV88fd2l35/5vcpoe5A444n6qb8ZaqzdBYXgyUPyVAbzcySKEm/
b1HuV8dhlOWZcwgGAdgf/yBhu8WN1Mau6zTAFK2osKUQM2TeXCDKX6tDAHryD6jA
MP/med+RSLJyyL5OYBl1P/gqSstH0HnpkpeYslaZpXncT2V2PHTwXOs2ywOESil6
yEi9KcsPe87hJ5aMJ0iw/A8AkDBnSzvx2LoYtgLxWl/+4xw=
-----END CERTIFICATE-----

17
public/ssl/client.csr Normal file
View File

@ -0,0 +1,17 @@
-----BEGIN CERTIFICATE REQUEST-----
MIICqTCCAZECAQAwZDELMAkGA1UEBhMCREUxEDAOBgNVBAgMB0dlcm1hbnkxETAP
BgNVBAcMCFdhbGxkb3JmMQ8wDQYDVQQKDAZTQVAgU0UxDjAMBgNVBAsMBVRvb2xz
MQ8wDQYDVQQDDAZjbGllbnQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
AQCZ7hsC/0FOfOUUyefqhFvmiYJoqMSi8/vvjJKn+TCXhlfX/BLxBtJN8BlFNDux
+qPlPhElbCg0bldeBGkZNgD7Jt6FjpkdSqqoDIcfl+oxAks76Qi5hh2FKHSOp3Bm
HgVuur+cbzOd8J+NsskGYay32mAHLrq8ixPLUtkOO9W2PSKm9KQEwOdYV9R/dStv
ZDA5dEVEDGv3MIBgRVzyu8gGwMfjzci1wgwU5Eb2r2b7Vs19nAoLQwelBf4bL5Z5
b2KjfW1HPhmtM1eBaf+3bMscnemAgY8I0ZHMS0XjORLvSBKZ73Q1K9lv6dc45fQA
6g3KnVvFSB0nfqbhw7vuDEXrAgMBAAGgADANBgkqhkiG9w0BAQsFAAOCAQEAMQNR
9TrgG1Re1z2nmxQ2lWPfpdx5wTFc2SibJhMGTahEhAW6XJEtYpTPE+S2LEIybr9k
Ya+Pg/Q6pE2MjCDMOSkmfXVcyu/Fw+Ek1anNQ1IDS68vVA3lUNpXYHPffZOTdlj6
15n7GMUdSISLk8jZOLGli26PLQimSzHeLUjHwFaS6fs5dXrASdDEAq+GfjXw+R83
Lh6ENb9ojdtnHhEspWsiuyJVT4GgV2U1q9m+ljZJe8fBgQP3exxVZXgnAeogpy4r
+pTjJIRuaxr3xnd/oYQDCaKpXptEuHQ5caQzFmmUM94sAZkQJQn5VVjSCfDv2ZHm
Rp+QdXH8iMjiaxKUaQ==
-----END CERTIFICATE REQUEST-----

27
public/ssl/client.key Normal file
View File

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEAme4bAv9BTnzlFMnn6oRb5omCaKjEovP774ySp/kwl4ZX1/wS
8QbSTfAZRTQ7sfqj5T4RJWwoNG5XXgRpGTYA+ybehY6ZHUqqqAyHH5fqMQJLO+kI
uYYdhSh0jqdwZh4Fbrq/nG8znfCfjbLJBmGst9pgBy66vIsTy1LZDjvVtj0ipvSk
BMDnWFfUf3Urb2QwOXRFRAxr9zCAYEVc8rvIBsDH483ItcIMFORG9q9m+1bNfZwK
C0MHpQX+Gy+WeW9io31tRz4ZrTNXgWn/t2zLHJ3pgIGPCNGRzEtF4zkS70gSme90
NSvZb+nXOOX0AOoNyp1bxUgdJ36m4cO77gxF6wIDAQABAoIBAQCEQ+Vv8Ncz0vug
nlEp4St6b2Pf/ExiXNV5I8gMj4FiYexvSUkZVCw3Df0YyuYUa3KTE372MfZl/v4R
aibIo++53s9L4ZjNY5A6L/GXgxiXngn5Y6a8i3IoLffhcByTkm5GlC77A05OAymG
Pz6eviUEIZ9r7IpGYhbTGdAqe92J5a72yaGK7+xzA/srX1a8C1qVqsoVBT/js99m
kNGZDbpjNLZhXAaze51+Z4ehwVDjJXHgxiCRvoZprxo49DTe/xKef5k1p8rEcv78
b56B5fYXOnIa2VavEJmEcuaX7uEAW2LT5/hRck/1ekhUBkhJ1BjzgYXqrbX9BiMO
wiSjn9pRAoGBAMtg9ewPESMLdKvxAQ9O6DEoRlN1KqHjwkoDZXM9Hw1dDWtxDsuC
MwvlzQEj0EVF1N5FedQVuaFVB6Bcq2tIcqvtYOBsflkF01Fe6CSgrqwmWps2tw4p
3TbFPNtXlQwUN0CcOzlCimo5dWoa5GT5VAEjXJBy1qmlZunbPlYJBf+pAoGBAMHB
2mdK5SKjvduEm+65W4VyhY1S40a8sGI9LcbCnMN1sZcOII0I/1CVsvJMZDj6XVbA
/dT76OkrL8uk20oYImNpfMSejiBUB4nLbhcI665jVUd9mG+H0v+wT9L6Nh9cvA8U
QNHHPGkzBFOqzXk59p8cFWoIIY5xzcPiydh7jyVzAoGBALwPn7e1wvnt6Ofphjpa
k7iI7mbT7CUgz5LTCyeBeEpKJlOYir7CWWOCDowkSr0TsqAKDHqB0FIPp2qw5k3h
AzBZ44ACst6s1Vfj87OS5ZIIMTZfZOvy6DxyLDEDDq7JrsHO9bCgCA/rq9f+n/2C
1BvtT/W+SyM58C0E7+Jsm6BpAoGBAJrP1T3q7aH7ytr23dCkcafylRsSO15trVER
KN5C6RaTl03mj8OgiL9OnShUOU/9W07R7P6cOMD1LL89/aj6F5/uzS4csdrKySsk
S9ZD3mv8GkuA4qdakxCRQ3aDTXNJmUlDGXeEBZTYmoBvXLWbxp3ixolt7cHu1EXL
kxNRxlJZAoGAO/Qzs10/joY5R7jadZdclVfleuD0Y5HmpEr2WgXvVwzB3QJqHd2i
lovuNtDpUal5ncDzDKdHacoED5gYncDIT6Quair3VfHsm3LlQWiZT86bQUWRBD+J
z/4ppzGkzePS4Rf01Cjb7RwlQ7uDCaVyPjvCbAVaPBYQVr3FvaYLJbM=
-----END RSA PRIVATE KEY-----

BIN
public/ssl/client.p12 Normal file

Binary file not shown.

22
public/ssl/rootCA.crt Normal file
View File

@ -0,0 +1,22 @@
-----BEGIN CERTIFICATE-----
MIIDqTCCApGgAwIBAgIUdULIRijfukG4cY7OZiPC02tL2WYwDQYJKoZIhvcNAQEL
BQAwZDELMAkGA1UEBhMCREUxEDAOBgNVBAgMB0dlcm1hbnkxETAPBgNVBAcMCFdh
bGxkb3JmMQ8wDQYDVQQKDAZTQVAgU0UxDjAMBgNVBAsMBVRvb2xzMQ8wDQYDVQQD
DAZyb290Q0EwHhcNMjAwMzI3MjExOTE0WhcNMjMwMTE1MjExOTE0WjBkMQswCQYD
VQQGEwJERTEQMA4GA1UECAwHR2VybWFueTERMA8GA1UEBwwIV2FsbGRvcmYxDzAN
BgNVBAoMBlNBUCBTRTEOMAwGA1UECwwFVG9vbHMxDzANBgNVBAMMBnJvb3RDQTCC
ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMgq1Lz8VFNo41q0zCrZPnXM
im10M9JYUGFl5s3xRP/70scuc7EpUhQPrQy6mIUEHes7uD6kRWwViM4JtpvebH52
T+caNNk6iTcYbXC1wENRJF/A64OHeVT1RJQxN0KqVQWMYoZLFS+R1JuXVnGj0J1B
jRBwJOL4MC/mYD90k/ik6r05OH8hATk+5DzzGpPK2pawpD6nU8q/X7C1XdRNZS05
7jv6Of8aonkKl6k26+zCWgHXitOagWB2sOTBH7moQEwJSWeLR5CTr/5//FSP5TT8
aR0RO1y0X/RwIif/bobBsnPZnjvpHgb83a+5ZbZt7PRz6hrvyoQofrhh2yNSogkC
AwEAAaNTMFEwHQYDVR0OBBYEFImeXkZLHA+SYuyLyBsR5cWgSL6GMB8GA1UdIwQY
MBaAFImeXkZLHA+SYuyLyBsR5cWgSL6GMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZI
hvcNAQELBQADggEBAICitWFs3JzAH42GA/45FBYjfKqXorQp22rzQ2nAXFw9nPWC
FXNIv6EUWW4SsV5AnEnOGqpC+14/sXTiSWJnqgVk8ZzeOw8is/52cigGSno7wgcX
9me72WZxlehYsf0gdx7vZAnyrFSfJ2Q/N6EAJ1LSZe92xB4A58O7dqfNPqgtZrU2
QufA81rGqr7LiWZGzPXTX8jLTV8JuXTs/yiDawSpoInasofTJMom5zdAjYoZJrcW
m+gz4yEshWzPl6qbVGvUWYdeWQ1KI9EZXUnxPzswPjqutGlE31QGcJDXvfBTeQS+
c0XmLDf22h43UaNzYRdWc3IcPLned3qNlBPI3qY=
-----END CERTIFICATE-----

27
public/ssl/rootCA.key Normal file
View File

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAyCrUvPxUU2jjWrTMKtk+dcyKbXQz0lhQYWXmzfFE//vSxy5z
sSlSFA+tDLqYhQQd6zu4PqRFbBWIzgm2m95sfnZP5xo02TqJNxhtcLXAQ1EkX8Dr
g4d5VPVElDE3QqpVBYxihksVL5HUm5dWcaPQnUGNEHAk4vgwL+ZgP3ST+KTqvTk4
fyEBOT7kPPMak8ralrCkPqdTyr9fsLVd1E1lLTnuO/o5/xqieQqXqTbr7MJaAdeK
05qBYHaw5MEfuahATAlJZ4tHkJOv/n/8VI/lNPxpHRE7XLRf9HAiJ/9uhsGyc9me
O+keBvzdr7lltm3s9HPqGu/KhCh+uGHbI1KiCQIDAQABAoIBAAVr240uzF3h9l5r
jSgP8DgijRE/13N3/t1UdDbZtQO131STtoBy3Q08C3TPzPe1T9YiLPBNZK/zuVvT
OYXpNUkLjUh2Fj5X7oV2fOhTk9x+4xxQzlAmqCub5PqahqOgl2LqFXULrYw3R+na
5HV5eVn4/4gVcXpQPIXfmHW/g451ZL444jcn1TQYnE0CxlUastc5kaltCR53TWY0
5UNGbbhnbDm5d04uYBFo+a+5MmYSVJ5KDf/BrUBMKcjPetnv0jQfoBao+oka2uie
42dqtRWJG2JiJXXG8sSCiLHW9qPgjp8wVgb79AkMpIjdlabQvaI5q/7pSVyf1x2h
bLhVI4ECgYEA58RINaEZkSYhwhUnvY1agBGHajmAAz8krgyPRGj/HzYmLbwUgZI7
OIuB6uiWgK3VtP49oVMA5szrkMW4V5pKgL9piSzCOIKtRImblOFQcqKxCt0MrSzx
D1kwJ1YIpFueZwZ9/ngZttKhIRRscNkJUSazeFi2gDrKjKXoms4LyLECgYEA3Ri5
kMeF3yNwJmSAmupPemeuExR8L00YC5B93WWUzNcJ2pVJhSHHAPZ4OB4Z+7iLoCpM
/5iWku+kySg6AugH6fWlDoC5mTXLTa11M+rwVv1INO5PSWbxPoMe7DMTtbYkhUxW
HMMXg1P9eS4a1fFiPHnEvT+ovKuOJ4MJIzwOxNkCgYAiw1gpYx6YnOWXXOD3F4qp
hveOwU0oL4Jq8MtUcYNCxTZ4yasxvCNR2esEtxpL9scFPNU1q2OJOtdigaWiziu6
n/tObf47x64Bh7pkXF9asnhnrrxGBWWq7a/BVrA5JtzdiyW+03jX6UPt2EhjrMou
9+UXegb1uNvEuOXowlsWIQKBgEcvx8eaxjqzIU/rOhEm8hIaQcz44ockTmKi0jOv
mjpd3llXicVou7dOpCSFZJ5MrAAUvpfpHEoRCMCPyCXZaXrl0ZAa2CdBT8Uh3UOr
GFkZ0d7g//xFPdV/yDwKsgTmsVmN24gFNJPfPhR/SLqrrpKELlk0nvKoVevY399N
Xf/5AoGBAMTYf04U30iMgjwrXCQAebCKA641DshyxplYiTZYddhGUigUslxrrSA9
stdRoYmwlmXwwtGEyu/064sZiOQap8+MtL/mUrdpHzZfy+xK883d6UnYe/iTONgc
j7kBt543WxzQSRW/1l58xgZWltJlCE5EZCKXA7fX+IoIQZQnYRI4
-----END RSA PRIVATE KEY-----

1
public/ssl/rootCA.srl Normal file
View File

@ -0,0 +1 @@
0DE7FFB7FF93E6FD83FED6EB3A1D83EBB6A82DC1

20
public/ssl/server.crt Normal file
View File

@ -0,0 +1,20 @@
-----BEGIN CERTIFICATE-----
MIIDUjCCAjoCFA3n/7f/k+b9g/7W6zodg+u2qC3AMA0GCSqGSIb3DQEBCwUAMGQx
CzAJBgNVBAYTAkRFMRAwDgYDVQQIDAdHZXJtYW55MREwDwYDVQQHDAhXYWxsZG9y
ZjEPMA0GA1UECgwGU0FQIFNFMQ4wDAYDVQQLDAVUb29sczEPMA0GA1UEAwwGcm9v
dENBMB4XDTIwMDMyNzIxMTkxNFoXDTIxMDgwOTIxMTkxNFowZzELMAkGA1UEBhMC
REUxEDAOBgNVBAgMB0dlcm1hbnkxETAPBgNVBAcMCFdhbGxkb3JmMQ8wDQYDVQQK
DAZTQVAgU0UxDjAMBgNVBAsMBVRvb2xzMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEi
MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCx+njjit6UtTPP8IImkVc5UAYc
3XKXVpCjWSqMSL3xj1O9zWPz4Ke+0mxH4mh6Ko0yO6+eBmzozSJUO1sU/Iz8v5T8
ZqqXANkF5v/zBjVMCPb6jiA9hLmJBpzGRB07fiuf17fI0lQ9HpuNNjsmm5x1fWBO
/D/KEM9218Bu9XkSAplIGg86xuvpdPpYLrxNbx9xWWlcRB7IRUIGfNbRFHWC0ryW
5kLzVSHhK3EYfAvak6mdIJ4iXySWuY4qaUE9/Iijud1JTuq9lKZS4qWdg7NmAGWH
bau2cSYWZeFc9ACAVNcE+YNLwzXyGIXCLgAtQ0vJCPj3Yf/lF9vvc2mQ114dAgMB
AAEwDQYJKoZIhvcNAQELBQADggEBAL/jf+OeGeXiX2f0ot3kYEe5XKflQb++8eop
iXbm6nqD9syWW6mpON1tZQ9EmIpT4dnh+D2+OFqM1QpF7zNZXRzIOrAfjKayq0yd
taA6zDdDUVPWAzHZz4R70UiMSXJFIDtKhWm7wEEjr72OgYC3nlYrvffhSS3pRrBF
kXRKpuuE9Yt60ciKeFssozS/wuflQ6fcDawTpwtzYU7z5p5B4KL1TmB6ZTXLfmD3
aotONmHOKqNUKdvgNfH9+09S3/bNsbSsA5epWjR9rm/PidRyk4x1UZEc3FAoSkGq
4r8TBc1LXsMk6TxUTRzEbtxCsoAllpPivi+cyGUNf+iF/FIWD+U=
-----END CERTIFICATE-----

17
public/ssl/server.csr Normal file
View File

@ -0,0 +1,17 @@
-----BEGIN CERTIFICATE REQUEST-----
MIICrDCCAZQCAQAwZzELMAkGA1UEBhMCREUxEDAOBgNVBAgMB0dlcm1hbnkxETAP
BgNVBAcMCFdhbGxkb3JmMQ8wDQYDVQQKDAZTQVAgU0UxDjAMBgNVBAsMBVRvb2xz
MRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
AoIBAQCx+njjit6UtTPP8IImkVc5UAYc3XKXVpCjWSqMSL3xj1O9zWPz4Ke+0mxH
4mh6Ko0yO6+eBmzozSJUO1sU/Iz8v5T8ZqqXANkF5v/zBjVMCPb6jiA9hLmJBpzG
RB07fiuf17fI0lQ9HpuNNjsmm5x1fWBO/D/KEM9218Bu9XkSAplIGg86xuvpdPpY
LrxNbx9xWWlcRB7IRUIGfNbRFHWC0ryW5kLzVSHhK3EYfAvak6mdIJ4iXySWuY4q
aUE9/Iijud1JTuq9lKZS4qWdg7NmAGWHbau2cSYWZeFc9ACAVNcE+YNLwzXyGIXC
LgAtQ0vJCPj3Yf/lF9vvc2mQ114dAgMBAAGgADANBgkqhkiG9w0BAQsFAAOCAQEA
I3455yEdVYmyv+9aDGNuUAPEKvnubLZOuEC6IweCT88f9cwQlvTNSkgQ8ylJ40oQ
D/akfBMQEc11NjVoRE5jFPabLMr0wC/KWL5RhXAwu82pC7l64jd8xLhHWXE1cY7h
i7pGBawcvnuqlkwPnd6OSh4c1MdHMsefBb8RKvDJ4I6iWfu1ZKBWWBknnTKwEB/y
K/jCq81z3xwlNx8r5MT43thzYMRxRIXX63Le28OurRWJNCxuyrQUh7dqGhbfXvOC
VCvFZphRc8bB9h45wvblPAgcVVDcqpKUGHvqUd456wQJ0JIer0VXhrAMIdzBiCzl
eqBFgozJ6u2jBWxvfrnHWQ==
-----END CERTIFICATE REQUEST-----

27
public/ssl/server.key Normal file
View File

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAsfp444relLUzz/CCJpFXOVAGHN1yl1aQo1kqjEi98Y9Tvc1j
8+CnvtJsR+JoeiqNMjuvngZs6M0iVDtbFPyM/L+U/GaqlwDZBeb/8wY1TAj2+o4g
PYS5iQacxkQdO34rn9e3yNJUPR6bjTY7JpucdX1gTvw/yhDPdtfAbvV5EgKZSBoP
Osbr6XT6WC68TW8fcVlpXEQeyEVCBnzW0RR1gtK8luZC81Uh4StxGHwL2pOpnSCe
Il8klrmOKmlBPfyIo7ndSU7qvZSmUuKlnYOzZgBlh22rtnEmFmXhXPQAgFTXBPmD
S8M18hiFwi4ALUNLyQj492H/5Rfb73NpkNdeHQIDAQABAoIBAD8+OebhWeaN3TNu
y1DZJJ2BCisHpciRQiRJcw0WbCiCPcecTIBEvFbafw+sLGP86t+GxgjpT5oKCsDT
trHmbFMD4PUvpj6yVmv6gcjh096I8Ppntp0lpKhEaUEqwxh45RePmAcMdlKhpbSw
KKS3dwlo2+g1SpWLE62vTPog0DzNh8WtFePAqt5SzA07fpuZSuhobchCHQpZS4Kr
Pv8mJj/uWSa3ysVqJ6FaqeIqMTNP5S5QxJw/OOLR+0Wzi8m2ET0Be16H7NO7EgXr
6xjY3GcI292oXylSFMSZkKuRf3fX08AP8hKsP7A7qjTY/S4gQ1Tte9B0s2Uq7LJx
liXmoAECgYEA3n4cfmCFZwe1NjpksKe8kLm871oq5x7UB3OsbIKPYvdp2/G3eour
175qe5oMJgJe28Jqgu7EE9zDQcGMSpV7yapbeGcIg5h7b2a1Un3TbvJIMBLBrbR6
fGwW+gL0REeLrl7vMgrUTXO1/MbiMvoa48fUea+PLW48qQ53qGh2TR0CgYEAzMgt
oY1pBTNim+0pgjQTwAi+y0Jirc6pAfunrf72onBy5Bt0arBsYMlKjZS6yaR78aqh
JNHymXRHwQZK9oMx6tLR8jT+E9LJH7Hk2Vv5M6f8xZwF/f0zc67mSEuj+mHc0X++
qd7lYmudyAfWM0+A3DUVAXUyMnoKZzvCO9fFhQECgYEAg5Vj9p3Q56EYW8znFc7t
503h3lCeRPfnf8y6caY5dNdMJQbscy49YCe+RAFUI/qM7T0qzuq0zeZnF/GGremA
P0FgPXH6CBHbFoRQwkumCtyBMuU05C1zrzgh0pSCsAr8IhEFN7xN2MyRGcDpsCpY
UtQw5hKdA8pJV9Y1kETPikUCgYA64z2r/Vw78KDksfiDxrH/QQSMstRposobFeEM
Ogt2fturGPILVBx2YKwdtq1YGwLBZg3c5rrawgN4UHTyGpwaKPHSssZ1sOHBSYjD
sJ0i66XWtZ1LgqpvE9aI56eJ8uZrIE8VzlEsUkIXKZnBO5WUvXcC6k67ETk4ooii
aNQWAQKBgQDOlUkeRQjVhm9fW8HrKdqUf4+1Zge4wwOi6q4fO4EdaSFu9KGnUOmS
crvagTexI5MPHuq7LuK4MsWrNlbxJ/axjazDDLqeaWaWOtJvO+L274yWQtRQ4/DY
tcTDKTbGln/v+x8eWW9OhUy/ADUFPkweXoemnI5iIdjymkZFGB/XlQ==
-----END RSA PRIVATE KEY-----

22
public/ssl/setup.sh Normal file
View File

@ -0,0 +1,22 @@
#!/bin/bash
# From https://github.com/thojansen/client-certificates/blob/master/ssl/setup.sh
# create rootCA certificate
openssl genrsa -out rootCA.key 2048
openssl req -x509 -new -nodes -key rootCA.key -days 1024 -out rootCA.crt -subj "/C=DE/ST=Germany/L=Walldorf/O=SAP SE/OU=Tools/CN=rootCA"
# create server key and certificate
openssl genrsa -out server.key 2048
openssl req -new -key server.key -out server.csr -subj "/C=DE/ST=Germany/L=Walldorf/O=SAP SE/OU=Tools/CN=localhost"
openssl x509 -req -in server.csr -CA rootCA.crt -CAkey rootCA.key -CAcreateserial -out server.crt -days 500
# create client key and certificate
openssl genrsa -out client.key 2048
openssl req -new -key client.key -out client.csr -subj "/C=DE/ST=Germany/L=Walldorf/O=SAP SE/OU=Tools/CN=client"
openssl x509 -req -in client.csr -CA rootCA.crt -CAkey rootCA.key -CAcreateserial -out client.crt -days 500
# generate client.p12 file which can be easily imported to OS.
openssl pkcs12 -export -inkey client.key -in client.crt -name client -out client.p12
# generate a non-encrypt pem file with key and crt files, from p12 files
#openssl pkcs12 -in client.p12 -out client.pem -nodes -clcerts

41
scripts/notarize.js Normal file
View File

@ -0,0 +1,41 @@
const fs = require("fs");
const path = require("path");
const { notarize } = require("electron-notarize");
const envConfig = require('dotenv').config();
Object.entries(envConfig.parsed || {}).forEach(([key, value]) => {
process.env[key] = value;
});
module.exports = async function (params) {
console.log(process.env);
// Only notarize the app on Mac OS only.
if (process.platform !== "darwin") {
return;
}
// Same appId in electron-builder.
let appId = "io.gnosis.safe.macos";
let appPath = path.join(
params.appOutDir,
`${params.packager.appInfo.productFilename}.app`
);
if (!fs.existsSync(appPath)) {
throw new Error(`Cannot find application at: ${appPath}`);
}
console.log(`Notarizing ${appId} found at ${appPath}`);
try {
await notarize({
appBundleId: appId,
appPath: appPath,
appleId: process.env.APPLEID,
appleIdPassword: process.env.APPLEIDPASS,
});
} catch (error) {
console.error(error);
}
console.log(`Done notarizing ${appId}`);
};

18
scripts/preload.js Normal file
View File

@ -0,0 +1,18 @@
// All of the Node.js APIs are available in the preload process.
// It has the same sandbox as a Chrome extension.
const TransportNodeHid = require("@ledgerhq/hw-transport-node-hid").default;
window.TransportNodeHid = TransportNodeHid;
window.isDesktop = true;
window.addEventListener('DOMContentLoaded', () => {
const replaceText = (selector, text) => {
const element = document.getElementById(selector)
if (element) element.innerText = text
}
for (const type of ['chrome', 'node', 'electron']) {
replaceText(`${type}-version`, process.versions[type])
}
})

View File

@ -4,7 +4,7 @@ import styled from 'styled-components'
export const Wrapper = styled.div`
display: grid;
grid-template-columns: 245px auto;
grid-template-rows: 500px;
grid-template-rows: 514px;
min-height: 525px;
.background {
@ -33,6 +33,7 @@ export const Menu = styled.div.attrs(() => ({ className: 'background' }))`
export const Content = styled.div.attrs(() => ({ className: 'background' }))`
grid-column: 2;
border-top-right-radius: 8px;
border-bottom-right-radius: 8px;
background-color: white;
`

View File

@ -63,36 +63,6 @@ const AddressInput = ({
}
}}
</OnChange>
{/* onBlur - didn't work because of the complex validation
(if you submit before it gets the address, breaks everything) */}
{/* <Field
name={name}
subscription={{ active: true, value: true }}
render={({ meta, input }) => {
const [prevActive, setPrevActive] = useState<boolean>(!!meta.active)
useEffect(() => {
async function setAddressFromENS() {
if (isValidEnsName(input.value)) {
try {
const resolverAddr = await getAddressFromENS(input.value)
fieldMutator(resolverAddr)
} catch (err) {
console.error('Error when trying to fetch address for ENS name: ', err)
}
}
}
if (prevActive && !meta.active) {
setAddressFromENS()
} else if (prevActive !== meta.active) {
setPrevActive(meta.active)
}
}, [meta.active])
return null
}}
/> */}
</>
)

View File

@ -7,7 +7,7 @@ import { Field } from 'react-final-form'
type Props = {
validate: () => void,
debounce: number,
debounce?: number,
}
const DebounceValidationField = ({ debounce = 1000, validate, ...rest }: Props) => {

View File

@ -88,12 +88,8 @@ export const uniqueAddress = (addresses: string[] | List<string>) =>
return addressAlreadyExists ? ADDRESS_REPEATED_ERROR : undefined
})
export const composeValidators = (...validators: Function[]): FieldValidator => (value: Field, values, meta) => {
if (!meta.modified) {
return
}
return validators.reduce((error, validator) => error || validator(value), undefined)
}
export const composeValidators = (...validators: Function[]): FieldValidator => (value: Field) =>
validators.reduce((error, validator) => error || validator(value), undefined)
export const inLimit = (limit: number, base: number, baseText: string, symbol: string = 'ETH') => (value: string) => {
const amount = Number(value)

View File

@ -118,8 +118,15 @@ export const estimateSafeTxGas = async (
gasLimit: txGasEstimation + dataGasEstimation + additionalGas,
},
(error, res) => {
// res.data check is for OpenEthereum/Parity revert messages format
const isOpenEthereumRevertMsg = res && typeof res.data === 'string'
const isEstimationSuccessful =
!error &&
((typeof res === 'string' && res !== '0x') || (isOpenEthereumRevertMsg && res.data.slice(9) !== '0x'))
resolve({
success: error || res === '0x' ? false : true,
success: isEstimationSuccessful,
estimation: txGasEstimation + additionalGas,
})
},

View File

@ -9,7 +9,7 @@ import { type Token, makeToken } from '~/logic/tokens/store/model/token'
export const TOKEN_REDUCER_ID = 'tokens'
export type State = Map<string, Map<string, Token>>
export type State = Map<string, Token>
export default handleActions<State, *>(
{

View File

@ -18,7 +18,7 @@ export const formatAmount = (number: string | number) => {
let numberFloat = parseFloat(number)
if (numberFloat === 0) {
numberFloat = '0.000'
numberFloat = '0'
} else if (numberFloat < 0.001) {
numberFloat = '< 0.001'
} else if (numberFloat < 1000) {

View File

@ -8,7 +8,7 @@ import OpenPaper from '~/components/Stepper/OpenPaper'
import AddressInput from '~/components/forms/AddressInput'
import Field from '~/components/forms/Field'
import TextField from '~/components/forms/TextField'
import { composeValidators, mustBeEthereumAddress, noErrorsOn, required } from '~/components/forms/validator'
import { mustBeEthereumAddress, noErrorsOn, required } from '~/components/forms/validator'
import Block from '~/components/layout/Block'
import Paragraph from '~/components/layout/Paragraph'
import { SAFE_MASTER_COPY_ADDRESS_V10, getSafeMasterContract, validateProxy } from '~/logic/contracts/safeContracts'
@ -120,7 +120,6 @@ const Details = ({ classes, errors, form }: Props) => (
placeholder="Safe Address*"
text="Safe Address"
type="text"
validate={composeValidators(required, mustBeEthereumAddress)}
/>
</Block>
<Block margin="sm">

View File

@ -1,5 +1,6 @@
// @flow
import { ButtonLink, Checkbox, ManageListModal, Text, TextField } from '@gnosis.pm/safe-react-components'
import type { FieldValidator } from 'final-form'
import React, { useState } from 'react'
import { FormSpy } from 'react-final-form'
import styled from 'styled-components'
@ -9,7 +10,7 @@ import { getAppInfoFromUrl } from './utils'
import Field from '~/components/forms/Field'
import DebounceValidationField from '~/components/forms/Field/DebounceValidationField'
import GnoForm from '~/components/forms/GnoForm'
import { composeValidators, required } from '~/components/forms/validator'
import { required } from '~/components/forms/validator'
import Img from '~/components/layout/Img'
import appsIconSvg from '~/routes/safe/components/Transactions/TxsTable/TxType/assets/appsIcon.svg'
@ -55,6 +56,13 @@ const urlValidator = (value: string) => {
: 'Please, provide a valid url'
}
const composeValidatorsApps = (...validators: Function[]): FieldValidator => (value: Field, values, meta) => {
if (!meta.modified) {
return
}
return validators.reduce((error, validator) => error || validator(value), undefined)
}
const ManageApps = ({ appList, onAppAdded, onAppToggle }: Props) => {
const [isOpen, setIsOpen] = useState(false)
@ -120,7 +128,12 @@ const ManageApps = ({ appList, onAppAdded, onAppToggle }: Props) => {
name="appUrl"
placeholder="App URL"
type="text"
validate={composeValidators(customRequiredValidator, urlValidator, uniqueAppValidator, safeAppValidator)}
validate={composeValidatorsApps(
customRequiredValidator,
urlValidator,
uniqueAppValidator,
safeAppValidator,
)}
/>
<AppInfo>

View File

@ -26,6 +26,8 @@ const APPS_STORAGE_KEY = 'APPS_STORAGE_KEY'
const APPS_LEGAL_DISCLAIMER_STORAGE_KEY = 'APPS_LEGAL_DISCLAIMER_STORAGE_KEY'
const StyledIframe = styled.iframe`
padding: 24px;
box-sizing: border-box;
width: 100%;
height: 100%;
display: ${(props) => (props.shouldDisplay ? 'block' : 'none')};
@ -36,7 +38,6 @@ const Centered = styled.div`
justify-content: center;
flex-direction: column;
`
const operations = {
SEND_TRANSACTIONS: 'SEND_TRANSACTIONS',
ON_SAFE_INFO: 'ON_SAFE_INFO',
@ -151,7 +152,7 @@ function Apps({ closeModal, closeSnackbar, enqueueSnackbar, openModal }: Props)
<a href="https://gnosis-safe.io/terms" rel="noopener noreferrer" target="_blank">
Terms
</a>{' '}
and this Disclaimer, and agree to be bound by .
and this Disclaimer, and agree to be bound by them.
</Text>
</>
}
@ -299,9 +300,8 @@ function Apps({ closeModal, closeSnackbar, enqueueSnackbar, openModal }: Props)
const currentApp = list[index]
const appInfo = await getAppInfoFromUrl(currentApp.url)
if (appInfo.error) {
throw Error()
throw Error(`There was a problem trying to load app ${currentApp.url}`)
}
appInfo.disabled = currentApp.disabled === undefined ? false : currentApp.disabled
@ -348,6 +348,10 @@ function Apps({ closeModal, closeSnackbar, enqueueSnackbar, openModal }: Props)
return <Loader />
}
if (loading || !appList.length) {
return <Loader />
}
return (
<>
<Menu>

View File

@ -3,10 +3,15 @@ import axios from 'axios'
import appsIconSvg from '~/routes/safe/components/Transactions/TxsTable/TxType/assets/appsIcon.svg'
export const GNOSIS_APPS_URL = 'https://gnosis-apps.netlify.app'
const appsUrl = process.env.REACT_APP_GNOSIS_APPS_URL ? process.env.REACT_APP_GNOSIS_APPS_URL : GNOSIS_APPS_URL
export const staticAppsList = [{ url: `${appsUrl}/compound`, disabled: false }]
const gnosisAppsUrl = process.env.REACT_APP_GNOSIS_APPS_URL
export const staticAppsList = [
{ url: `${gnosisAppsUrl}/compound`, disabled: false },
{ url: `${gnosisAppsUrl}/aave`, disabled: false },
{ url: `${gnosisAppsUrl}/pool-together`, disabled: false },
{ url: `${gnosisAppsUrl}/open-zeppelin`, disabled: false },
{ url: `${gnosisAppsUrl}/request`, disabled: false },
{ url: `${gnosisAppsUrl}/synthetix`, disabled: false },
]
export const getAppInfoFromOrigin = (origin: string) => {
try {
@ -18,13 +23,18 @@ export const getAppInfoFromOrigin = (origin: string) => {
}
export const getAppInfoFromUrl = async (appUrl: string) => {
let res = { id: undefined, url: appUrl, name: 'unknown', iconUrl: appsIconSvg, error: true }
if (!appUrl) {
return res
}
let cleanedUpAppUrl = appUrl.trim()
if (cleanedUpAppUrl.substr(-1) === '/') {
cleanedUpAppUrl = cleanedUpAppUrl.substr(0, cleanedUpAppUrl.length - 1)
res.url = cleanedUpAppUrl
}
let res = { id: undefined, url: cleanedUpAppUrl, name: 'unknown', iconUrl: appsIconSvg, error: true }
try {
const appInfo = await axios.get(`${cleanedUpAppUrl}/manifest.json`)

View File

@ -21,6 +21,7 @@ import Img from '~/components/layout/Img'
import Paragraph from '~/components/layout/Paragraph'
import Row from '~/components/layout/Row'
import { type Token, type TokenProps } from '~/logic/tokens/store/model/token'
import { getWeb3 } from '~/logic/wallets/getWeb3'
import TokenPlaceholder from '~/routes/safe/components/Balances/assets/token_placeholder.svg'
export const ADD_CUSTOM_TOKEN_ADDRESS_INPUT_TEST_ID = 'add-custom-token-address-input'
@ -64,8 +65,9 @@ const AddCustomToken = (props: Props) => {
const [formValues, setFormValues] = useState(INITIAL_FORM_STATE)
const handleSubmit = (values) => {
const address = getWeb3().utils.toChecksumAddress(values.address)
const token = {
address: values.address,
address,
decimals: values.decimals,
symbol: values.symbol,
name: values.symbol,

View File

@ -100,7 +100,7 @@ const TabsComponent = (props: Props) => {
label={labelTransactions}
value={`${match.url}/transactions`}
/>
{process.env.REACT_APP_ENV !== 'production' && (
{!process.env.REACT_APP_APPS_DISABLED && (
<Tab
classes={{
selected: classes.tabWrapperSelected,

View File

@ -91,7 +91,7 @@ const Layout = (props: Props) => {
<Switch>
<Route exact path={`${match.path}/balances/:assetType?`} render={() => wrapInSuspense(<Balances />, null)} />
<Route exact path={`${match.path}/transactions`} render={() => wrapInSuspense(<TxsTable />, null)} />
{process.env.REACT_APP_ENV !== 'production' && (
{!process.env.REACT_APP_APPS_DISABLED && (
<Route
exact
path={`${match.path}/apps`}

View File

@ -49,7 +49,7 @@ const IncomingTxDescription = ({ tx }: Props) => {
const txFromName = getNameFromAddressBook(tx.from)
return (
<Block className={classes.txDataContainer}>
<TransferDescription from={tx.from} txFromName={txFromName} value={getIncomingTxAmount(tx)} />
<TransferDescription from={tx.from} txFromName={txFromName} value={getIncomingTxAmount(tx, false)} />
</Block>
)
}

View File

@ -222,7 +222,7 @@ const TxDescription = ({ classes, tx }: Props) => {
removedOwner,
upgradeTx,
} = getTxData(tx)
const amount = getTxAmount(tx)
const amount = getTxAmount(tx, false)
return (
<Block className={classes.txDataContainer}>
{modifySettingsTx && action && (

View File

@ -41,8 +41,9 @@ const ExpandedTx = ({ cancelTx, tx }: Props) => {
const [openModal, setOpenModal] = useState<OpenModal>(null)
const openApproveModal = () => setOpenModal('approveTx')
const closeModal = () => setOpenModal(null)
const thresholdReached = !INCOMING_TX_TYPES.includes(tx.type) && threshold <= tx.confirmations.size
const canExecute = !INCOMING_TX_TYPES.includes(tx.type) && nonce === tx.nonce
const isIncomingTx = !!INCOMING_TX_TYPES[tx.type]
const thresholdReached = !isIncomingTx && threshold <= tx.confirmations.size
const canExecute = !isIncomingTx && nonce === tx.nonce
const cancelThresholdReached = !!cancelTx && threshold <= cancelTx.confirmations.size
const canExecuteCancel = nonce === tx.nonce
@ -59,22 +60,22 @@ const ExpandedTx = ({ cancelTx, tx }: Props) => {
<Block className={classes.expandedTxBlock}>
<Row>
<Col layout="column" xs={6}>
<Block
className={cn(classes.txDataContainer, INCOMING_TX_TYPES.includes(tx.type) && classes.incomingTxBlock)}
>
<Block className={cn(classes.txDataContainer, isIncomingTx && classes.incomingTxBlock)}>
<Block align="left" className={classes.txData}>
<Bold className={classes.txHash}>Hash:</Bold>
{tx.executionTxHash ? <EtherScanLink cut={8} type="tx" value={tx.executionTxHash} /> : 'n/a'}
</Block>
<Paragraph noMargin>
<Bold>Nonce: </Bold>
<Span>{tx.nonce}</Span>
</Paragraph>
{!isIncomingTx && (
<Paragraph noMargin>
<Bold>Nonce: </Bold>
<Span>{tx.nonce}</Span>
</Paragraph>
)}
<Paragraph noMargin>
<Bold>Fee: </Bold>
{tx.fee ? tx.fee : 'n/a'}
</Paragraph>
{INCOMING_TX_TYPES.includes(tx.type) ? (
{isIncomingTx ? (
<>
<Paragraph noMargin>
<Bold>Created: </Bold>
@ -113,9 +114,9 @@ const ExpandedTx = ({ cancelTx, tx }: Props) => {
)}
</Block>
<Hairline />
{INCOMING_TX_TYPES.includes(tx.type) ? <IncomingTxDescription tx={tx} /> : <TxDescription tx={tx} />}
{isIncomingTx ? <IncomingTxDescription tx={tx} /> : <TxDescription tx={tx} />}
</Col>
{!INCOMING_TX_TYPES.includes(tx.type) && (
{!isIncomingTx && (
<OwnersColumn
cancelThresholdReached={cancelThresholdReached}
cancelTx={cancelTx}

View File

@ -31,26 +31,31 @@ const typeToLabel = {
}
const TxType = ({ origin, txType }: { txType: TransactionType, origin: string | null }) => {
const isThirdPartyApp = txType === 'third-party-app'
const [loading, setLoading] = useState(true)
const [appInfo, setAppInfo] = useState()
const [forceCustom, setForceCustom] = useState(false)
useEffect(() => {
const getAppInfo = async () => {
const parsedOrigin = getAppInfoFromOrigin(origin)
if (!parsedOrigin) {
setForceCustom(true)
setLoading(false)
return
}
const appInfo = await getAppInfoFromUrl(parsedOrigin.url)
setAppInfo(appInfo)
setLoading(false)
}
if (!isThirdPartyApp) {
if (!origin) {
return
}
getAppInfo()
}, [txType])
}, [origin, txType])
if (!isThirdPartyApp) {
if (forceCustom || !origin) {
return <IconText iconUrl={typeToIcon[txType]} text={typeToLabel[txType]} />
}

View File

@ -8,7 +8,7 @@ import TxType from './TxType'
import { type Column } from '~/components/Table/TableHead'
import { type SortRow, buildOrderFieldFrom } from '~/components/Table/sorting'
import { getWeb3 } from '~/logic/wallets/getWeb3'
import { formatAmount } from '~/logic/tokens/utils/formatAmount'
import { INCOMING_TX_TYPES, type IncomingTransaction } from '~/routes/safe/store/models/incomingTransaction'
import { type Transaction } from '~/routes/safe/store/models/transaction'
@ -33,25 +33,40 @@ type TxData = {
export const formatDate = (date: string): string => format(parseISO(date), 'MMM d, yyyy - HH:mm:ss')
export const getIncomingTxAmount = (tx: IncomingTransaction) => {
const txAmount = tx.value ? `${new BigNumber(tx.value).div(`1e${tx.decimals}`).toFixed()}` : 'n/a'
return `${txAmount} ${tx.symbol || 'n/a'}`
type TxValues = {
value?: string | number,
decimals?: string | number,
symbol?: string,
}
export const getTxAmount = (tx: Transaction) => {
const web3 = getWeb3()
const { fromWei, toBN } = web3.utils
const NOT_AVAILABLE = 'n/a'
let txAmount = 'n/a'
const getAmountWithSymbol = ({ decimals = 0, symbol = NOT_AVAILABLE, value }: TxValues, formatted = false) => {
const nonFormattedValue = BigNumber(value).times(`1e-${decimals}`).toFixed()
const finalValue = formatted ? formatAmount(nonFormattedValue).toString() : nonFormattedValue
const txAmount = finalValue === 'NaN' ? NOT_AVAILABLE : finalValue
if (tx.isTokenTransfer && tx.decodedParams) {
const tokenDecimals = tx.decimals.toNumber ? tx.decimals.toNumber() : tx.decimals
txAmount = `${new BigNumber(tx.decodedParams.value).div(10 ** tokenDecimals).toString()} ${tx.symbol}`
} else if (Number(tx.value) > 0) {
txAmount = `${fromWei(toBN(tx.value), 'ether')} ${tx.symbol}`
return `${txAmount} ${symbol}`
}
export const getIncomingTxAmount = (tx: IncomingTransaction, formatted: boolean = true) => {
// simple workaround to avoid displaying unexpected values for incoming NFT transfer
if (INCOMING_TX_TYPES[tx.type] === INCOMING_TX_TYPES.ERC721_TRANSFER) {
return `1 ${tx.symbol}`
}
return txAmount
return getAmountWithSymbol(tx, formatted)
}
export const getTxAmount = (tx: Transaction, formatted: boolean = true) => {
const { decimals = 18, decodedParams, isTokenTransfer, symbol } = tx
const { value } = isTokenTransfer && decodedParams && decodedParams.value ? decodedParams : tx
if (!isTokenTransfer && !(Number(value) > 0)) {
return NOT_AVAILABLE
}
return getAmountWithSymbol({ decimals, symbol, value }, formatted)
}
export type TransactionRow = SortRow<TxData>
@ -75,7 +90,7 @@ const getTransactionTableData = (tx: Transaction, cancelTx: ?Transaction): Trans
} else if (tx.cancellationTx) {
txType = 'cancellation'
} else if (tx.customTx) {
txType = tx.origin ? 'third-party-app' : 'custom'
txType = 'custom'
} else if (tx.creationTx) {
txType = 'creation'
} else if (tx.upgradeTx) {
@ -101,7 +116,7 @@ export const getTxTableData = (
const cancelTxsByNonce = cancelTxs.reduce((acc, tx) => acc.set(tx.nonce, tx), Map())
return transactions.map((tx) => {
if (INCOMING_TX_TYPES.includes(tx.type)) {
if (INCOMING_TX_TYPES[tx.type]) {
return getIncomingTxTableData(tx)
}

View File

@ -76,12 +76,12 @@ type IncomingTxServiceModel = {
export const buildTransactionFrom = async (
safeAddress: string,
tx: TxServiceModel,
knownTokens,
tx: TxServiceModel,
txTokenCode,
txTokenDecimals,
txTokenSymbol,
txTokenName,
code,
txTokenSymbol,
): Promise<Transaction> => {
const localSafe = await getLocalSafe(safeAddress)
@ -108,7 +108,7 @@ export const buildTransactionFrom = async (
const modifySettingsTx = sameAddress(tx.to, safeAddress) && Number(tx.value) === 0 && !!tx.data
const cancellationTx = sameAddress(tx.to, safeAddress) && Number(tx.value) === 0 && !tx.data
const isERC721Token =
(code && code.includes(SAFE_TRANSFER_FROM_WITHOUT_DATA_HASH)) ||
(txTokenCode && txTokenCode.includes(SAFE_TRANSFER_FROM_WITHOUT_DATA_HASH)) ||
(isTokenTransfer(tx.data, Number(tx.value)) && !knownTokens.get(tx.to) && txTokenDecimals !== null)
let isSendTokenTx = !isERC721Token && isTokenTransfer(tx.data, Number(tx.value))
const isMultiSendTx = isMultisendTransaction(tx.data, Number(tx.value))
@ -118,7 +118,7 @@ export const buildTransactionFrom = async (
let refundParams = null
if (tx.gasPrice > 0) {
const refundSymbol = txTokenSymbol || 'ETH'
const decimals = txTokenName || 18
const decimals = txTokenDecimals || 18
const feeString = (tx.gasPrice * (tx.baseGas + tx.safeTxGas)).toString().padStart(decimals, 0)
const whole = feeString.slice(0, feeString.length - decimals) || '0'
const fraction = feeString.slice(feeString.length - decimals)
@ -133,24 +133,26 @@ export const buildTransactionFrom = async (
let symbol = txTokenSymbol || 'ETH'
let decimals = txTokenDecimals || 18
let decodedParams
if (isSendTokenTx && (txTokenSymbol === null || txTokenDecimals === null)) {
try {
const [tokenSymbol, tokenDecimals] = await Promise.all(
generateBatchRequests({
abi: ALTERNATIVE_TOKEN_ABI,
address: tx.to,
methods: ['symbol', 'decimals'],
}),
)
if (isSendTokenTx) {
if (txTokenSymbol === null || txTokenDecimals === null) {
try {
const [tokenSymbol, tokenDecimals] = await Promise.all(
generateBatchRequests({
abi: ALTERNATIVE_TOKEN_ABI,
address: tx.to,
methods: ['symbol', 'decimals'],
}),
)
symbol = tokenSymbol
decimals = tokenDecimals
} catch (e) {
// some contracts may implement the same methods as in ERC20 standard
// we may falsely treat them as tokens, so in case we get any errors when getting token info
// we fallback to displaying custom transaction
isSendTokenTx = false
customTx = true
symbol = tokenSymbol
decimals = tokenDecimals
} catch (e) {
// some contracts may implement the same methods as in ERC20 standard
// we may falsely treat them as tokens, so in case we get any errors when getting token info
// we fallback to displaying custom transaction
isSendTokenTx = false
customTx = true
}
}
const params = web3.eth.abi.decodeParameters(['address', 'uint256'], tx.data.slice(10))
@ -228,8 +230,8 @@ const addMockSafeCreationTx = (safeAddress): Array<TxServiceModel> => [
const batchRequestTxsData = (txs: any[]) => {
const web3Batch = new web3.BatchRequest()
const whenTxsValues = txs.map((tx) => {
const methods = ['decimals', { method: 'getCode', type: 'eth', args: [tx.to] }, 'symbol', 'name']
const txsTokenInfo = txs.map((tx) => {
const methods = [{ method: 'getCode', type: 'eth', args: [tx.to] }, 'decimals', 'name', 'symbol']
return generateBatchRequests({
abi: ERC20Detailed.abi,
address: tx.to,
@ -241,7 +243,7 @@ const batchRequestTxsData = (txs: any[]) => {
web3Batch.execute()
return Promise.all(whenTxsValues)
return Promise.all(txsTokenInfo)
}
const batchRequestIncomingTxsData = (txs: IncomingTxServiceModel[]) => {
@ -339,9 +341,15 @@ export const loadSafeTransactions = async (safeAddress: string, getState: GetSta
const txsWithData = await batchRequestTxsData(transactions)
// In case that the etags don't match, we parse the new transactions and save them to the cache
const txsRecord: Array<RecordInstance<TransactionProps>> = await Promise.all(
txsWithData.map(([tx: TxServiceModel, decimals, symbol, name, code]) =>
buildTransactionFrom(safeAddress, tx, knownTokens, decimals, symbol, name, code),
),
txsWithData.map(([tx: TxServiceModel, code, decimals, name, symbol]) => {
const knownToken = knownTokens.get(tx.to)
if (knownToken) {
;({ decimals, name, symbol } = knownToken)
}
return buildTransactionFrom(safeAddress, knownTokens, tx, code, decimals, name, symbol)
}),
)
const groupedTxs = List(txsRecord).groupBy((tx) => (tx.get('cancellationTx') ? 'cancel' : 'outgoing'))

View File

@ -2,7 +2,12 @@
import { Record } from 'immutable'
import type { RecordFactory, RecordOf } from 'immutable'
export const INCOMING_TX_TYPES = ['INCOMING', 'ERC721_TRANSFER', 'ERC20_TRANSFER', 'ETHER_TRANSFER']
export const INCOMING_TX_TYPES = {
INCOMING: 'INCOMING',
ERC721_TRANSFER: 'ERC721_TRANSFER',
ERC20_TRANSFER: 'ERC20_TRANSFER',
ETHER_TRANSFER: 'ETHER_TRANSFER',
}
export type IncomingTransactionProps = {
blockNumber: number,

View File

@ -7,14 +7,7 @@ import { type Confirmation } from '~/routes/safe/store/models/confirmation'
export const OUTGOING_TX_TYPE = 'outgoing'
export type TransactionType =
| 'incoming'
| 'outgoing'
| 'settings'
| 'custom'
| 'creation'
| 'cancellation'
| 'third-party-app'
export type TransactionType = 'incoming' | 'outgoing' | 'settings' | 'custom' | 'creation' | 'cancellation'
export type TransactionStatus =
| 'awaiting_your_confirmation'
@ -24,7 +17,6 @@ export type TransactionStatus =
| 'cancelled'
| 'awaiting_execution'
| 'pending'
| 'third-party-app'
export type TransactionProps = {
nonce: ?number,

4061
yarn.lock

File diff suppressed because it is too large Load Diff