diff --git a/.env.example b/.env.example
index 504acbd5..9fc7c229 100644
--- a/.env.example
+++ b/.env.example
@@ -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
diff --git a/.github/ISSUE_TEMPLATE/workflows/release.yml b/.github/ISSUE_TEMPLATE/workflows/release.yml
new file mode 100644
index 00000000..550e1514
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/workflows/release.yml
@@ -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') }}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 6a4aecbd..8442f310 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -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 }}
diff --git a/.gitignore b/.gitignore
index 7e552830..f83c31b4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,9 +1,11 @@
node_modules/
build_webpack/
.DS_Store
-build/
+./build
yarn-error.log
.env*
.idea/
+dist
+electron-builder.yml
.yalc/
-yalc.lock
\ No newline at end of file
+yalc.lock
diff --git a/.travis.yml b/.travis.yml
index 8ecf3dc7..f907cbe5 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -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
diff --git a/config/webpack.config.prod.js b/config/webpack.config.prod.js
index d7a7f957..9d3f67c5 100644
--- a/config/webpack.config.prod.js
+++ b/config/webpack.config.prod.js
@@ -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)
diff --git a/package.json b/package.json
index 326f60a2..7b34c5c1 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/public/auto-updater/index.js b/public/auto-updater/index.js
new file mode 100644
index 00000000..7713063e
--- /dev/null
+++ b/public/auto-updater/index.js
@@ -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,
+};
diff --git a/public/build/all-certs.p12 b/public/build/all-certs.p12
new file mode 100644
index 00000000..6f79543d
Binary files /dev/null and b/public/build/all-certs.p12 differ
diff --git a/public/build/background.png b/public/build/background.png
new file mode 100644
index 00000000..4c0b9ba3
Binary files /dev/null and b/public/build/background.png differ
diff --git a/public/build/entitlements.mac.plist b/public/build/entitlements.mac.plist
new file mode 100644
index 00000000..0922e997
--- /dev/null
+++ b/public/build/entitlements.mac.plist
@@ -0,0 +1,22 @@
+
+
+
+
+ com.apple.security.network.client
+
+ com.apple.security.network.server
+
+ com.apple.security.cs.allow-jit
+
+ com.apple.security.cs.allow-unsigned-executable-memory
+
+ com.apple.security.device.camera
+
+ com.apple.security.device.microphone
+
+ com.apple.security.cs.allow-dyld-environment-variables
+
+ com.apple.security.cs.disable-library-validation
+
+
+
\ No newline at end of file
diff --git a/public/build/icon.icns b/public/build/icon.icns
new file mode 100644
index 00000000..a929bcba
Binary files /dev/null and b/public/build/icon.icns differ
diff --git a/public/build/icon.ico b/public/build/icon.ico
new file mode 100644
index 00000000..40a1cc30
Binary files /dev/null and b/public/build/icon.ico differ
diff --git a/public/build/safe.png b/public/build/safe.png
new file mode 100644
index 00000000..1fb98552
Binary files /dev/null and b/public/build/safe.png differ
diff --git a/public/electron.js b/public/electron.js
new file mode 100644
index 00000000..c0f1926c
--- /dev/null
+++ b/public/electron.js
@@ -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('');
+ }
+
+ 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();
+ }
+});
\ No newline at end of file
diff --git a/public/ssl/client.crt b/public/ssl/client.crt
new file mode 100644
index 00000000..85044731
--- /dev/null
+++ b/public/ssl/client.crt
@@ -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-----
diff --git a/public/ssl/client.csr b/public/ssl/client.csr
new file mode 100644
index 00000000..316742b4
--- /dev/null
+++ b/public/ssl/client.csr
@@ -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-----
diff --git a/public/ssl/client.key b/public/ssl/client.key
new file mode 100644
index 00000000..21f2401d
--- /dev/null
+++ b/public/ssl/client.key
@@ -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-----
diff --git a/public/ssl/client.p12 b/public/ssl/client.p12
new file mode 100644
index 00000000..019a931b
Binary files /dev/null and b/public/ssl/client.p12 differ
diff --git a/public/ssl/rootCA.crt b/public/ssl/rootCA.crt
new file mode 100644
index 00000000..1290586a
--- /dev/null
+++ b/public/ssl/rootCA.crt
@@ -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-----
diff --git a/public/ssl/rootCA.key b/public/ssl/rootCA.key
new file mode 100644
index 00000000..a93aab30
--- /dev/null
+++ b/public/ssl/rootCA.key
@@ -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-----
diff --git a/public/ssl/rootCA.srl b/public/ssl/rootCA.srl
new file mode 100644
index 00000000..58b8b9d9
--- /dev/null
+++ b/public/ssl/rootCA.srl
@@ -0,0 +1 @@
+0DE7FFB7FF93E6FD83FED6EB3A1D83EBB6A82DC1
diff --git a/public/ssl/server.crt b/public/ssl/server.crt
new file mode 100644
index 00000000..c400ffb0
--- /dev/null
+++ b/public/ssl/server.crt
@@ -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-----
diff --git a/public/ssl/server.csr b/public/ssl/server.csr
new file mode 100644
index 00000000..ee3d97b8
--- /dev/null
+++ b/public/ssl/server.csr
@@ -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-----
diff --git a/public/ssl/server.key b/public/ssl/server.key
new file mode 100644
index 00000000..97986765
--- /dev/null
+++ b/public/ssl/server.key
@@ -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-----
diff --git a/public/ssl/setup.sh b/public/ssl/setup.sh
new file mode 100644
index 00000000..e19029da
--- /dev/null
+++ b/public/ssl/setup.sh
@@ -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
\ No newline at end of file
diff --git a/scripts/notarize.js b/scripts/notarize.js
new file mode 100644
index 00000000..be2d6d0b
--- /dev/null
+++ b/scripts/notarize.js
@@ -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}`);
+};
\ No newline at end of file
diff --git a/scripts/preload.js b/scripts/preload.js
new file mode 100644
index 00000000..322e2fa7
--- /dev/null
+++ b/scripts/preload.js
@@ -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])
+ }
+})
\ No newline at end of file
diff --git a/src/components-v2/layouts/ListContentLayout/Layout.jsx b/src/components-v2/layouts/ListContentLayout/Layout.jsx
index 28ad2275..0228ed6d 100644
--- a/src/components-v2/layouts/ListContentLayout/Layout.jsx
+++ b/src/components-v2/layouts/ListContentLayout/Layout.jsx
@@ -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;
`
diff --git a/src/components/forms/AddressInput/index.jsx b/src/components/forms/AddressInput/index.jsx
index e371de9a..b947cc67 100644
--- a/src/components/forms/AddressInput/index.jsx
+++ b/src/components/forms/AddressInput/index.jsx
@@ -63,36 +63,6 @@ const AddressInput = ({
}
}}
- {/* onBlur - didn't work because of the complex validation
- (if you submit before it gets the address, breaks everything) */}
- {/* {
- const [prevActive, setPrevActive] = useState(!!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
- }}
- /> */}
>
)
diff --git a/src/components/forms/Field/DebounceValidationField.js b/src/components/forms/Field/DebounceValidationField.js
index 325946f5..7f32f379 100644
--- a/src/components/forms/Field/DebounceValidationField.js
+++ b/src/components/forms/Field/DebounceValidationField.js
@@ -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) => {
diff --git a/src/components/forms/validator.js b/src/components/forms/validator.js
index 9a8c78fa..0a065a4e 100644
--- a/src/components/forms/validator.js
+++ b/src/components/forms/validator.js
@@ -88,12 +88,8 @@ export const uniqueAddress = (addresses: string[] | List) =>
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)
diff --git a/src/logic/safe/transactions/gasNew.js b/src/logic/safe/transactions/gasNew.js
index 15c33c17..3f34178f 100644
--- a/src/logic/safe/transactions/gasNew.js
+++ b/src/logic/safe/transactions/gasNew.js
@@ -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,
})
},
diff --git a/src/logic/tokens/store/reducer/tokens.js b/src/logic/tokens/store/reducer/tokens.js
index 3d9d1916..fa4f47a2 100644
--- a/src/logic/tokens/store/reducer/tokens.js
+++ b/src/logic/tokens/store/reducer/tokens.js
@@ -9,7 +9,7 @@ import { type Token, makeToken } from '~/logic/tokens/store/model/token'
export const TOKEN_REDUCER_ID = 'tokens'
-export type State = Map>
+export type State = Map
export default handleActions(
{
diff --git a/src/logic/tokens/utils/formatAmount.js b/src/logic/tokens/utils/formatAmount.js
index 6c4221af..a6b5e628 100644
--- a/src/logic/tokens/utils/formatAmount.js
+++ b/src/logic/tokens/utils/formatAmount.js
@@ -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) {
diff --git a/src/routes/load/components/DetailsForm/index.jsx b/src/routes/load/components/DetailsForm/index.jsx
index 1d005ca8..266f131e 100644
--- a/src/routes/load/components/DetailsForm/index.jsx
+++ b/src/routes/load/components/DetailsForm/index.jsx
@@ -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)}
/>
diff --git a/src/routes/safe/components/Apps/ManageApps.js b/src/routes/safe/components/Apps/ManageApps.js
index fbacace9..972477ab 100644
--- a/src/routes/safe/components/Apps/ManageApps.js
+++ b/src/routes/safe/components/Apps/ManageApps.js
@@ -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,
+ )}
/>
diff --git a/src/routes/safe/components/Apps/index.jsx b/src/routes/safe/components/Apps/index.jsx
index 4b30eda0..258c4f5f 100644
--- a/src/routes/safe/components/Apps/index.jsx
+++ b/src/routes/safe/components/Apps/index.jsx
@@ -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)
Terms
{' '}
- and this Disclaimer, and agree to be bound by .
+ and this Disclaimer, and agree to be bound by them.
>
}
@@ -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
}
+ if (loading || !appList.length) {
+ return
+ }
+
return (
<>