Switch to React UI
|
@ -0,0 +1,52 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
.eslintcache
|
||||
|
||||
# Dependency directory
|
||||
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
|
||||
node_modules
|
||||
|
||||
# OSX
|
||||
.DS_Store
|
||||
|
||||
# flow-typed
|
||||
flow-typed/npm/*
|
||||
!flow-typed/npm/module_vx.x.x.js
|
||||
|
||||
# App packaged
|
||||
release
|
||||
app/main.prod.js
|
||||
app/main.prod.js.map
|
||||
app/renderer.prod.js
|
||||
app/renderer.prod.js.map
|
||||
app/style.css
|
||||
app/style.css.map
|
||||
dist
|
||||
dll
|
||||
main.js
|
||||
main.js.map
|
||||
|
||||
.idea
|
||||
npm-debug.log.*
|
||||
.*.dockerfile
|
|
@ -0,0 +1,12 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
|
@ -0,0 +1,56 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
.eslintcache
|
||||
|
||||
# Dependency directory
|
||||
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
|
||||
node_modules
|
||||
|
||||
# OSX
|
||||
.DS_Store
|
||||
|
||||
# flow-typed
|
||||
flow-typed/npm/*
|
||||
!flow-typed/npm/module_vx.x.x.js
|
||||
|
||||
# App packaged
|
||||
release
|
||||
app/main.prod.js
|
||||
app/main.prod.js.map
|
||||
app/renderer.prod.js
|
||||
app/renderer.prod.js.map
|
||||
app/style.css
|
||||
app/style.css.map
|
||||
dist
|
||||
dll
|
||||
main.js
|
||||
main.js.map
|
||||
|
||||
.idea
|
||||
npm-debug.log.*
|
||||
__snapshots__
|
||||
|
||||
# Package.json
|
||||
package.json
|
||||
.travis.yml
|
|
@ -0,0 +1,10 @@
|
|||
module.exports = {
|
||||
extends: 'erb',
|
||||
settings: {
|
||||
'import/resolver': {
|
||||
webpack: {
|
||||
config: require.resolve('./configs/webpack.config.eslint.js')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
[ignore]
|
||||
<PROJECT_ROOT>/app/main.prod.js
|
||||
<PROJECT_ROOT>/app/main.prod.js.map
|
||||
<PROJECT_ROOT>/app/dist/.*
|
||||
<PROJECT_ROOT>/resources/.*
|
||||
<PROJECT_ROOT>/node_modules/webpack-cli
|
||||
<PROJECT_ROOT>/release/.*
|
||||
<PROJECT_ROOT>/dll/.*
|
||||
<PROJECT_ROOT>/release/.*
|
||||
<PROJECT_ROOT>/git/.*
|
||||
|
||||
[include]
|
||||
|
||||
[libs]
|
||||
|
||||
[options]
|
||||
esproposal.class_static_fields=enable
|
||||
esproposal.class_instance_fields=enable
|
||||
esproposal.export_star_as=enable
|
||||
module.name_mapper.extension='css' -> '<PROJECT_ROOT>/internals/flow/CSSModule.js.flow'
|
||||
module.name_mapper.extension='styl' -> '<PROJECT_ROOT>/internals/flow/CSSModule.js.flow'
|
||||
module.name_mapper.extension='scss' -> '<PROJECT_ROOT>/internals/flow/CSSModule.js.flow'
|
||||
module.name_mapper.extension='png' -> '<PROJECT_ROOT>/internals/flow/WebpackAsset.js.flow'
|
||||
module.name_mapper.extension='jpg' -> '<PROJECT_ROOT>/internals/flow/WebpackAsset.js.flow'
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowIssue
|
|
@ -0,0 +1,6 @@
|
|||
* text eol=lf
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.jpeg binary
|
||||
*.ico binary
|
||||
*.icns binary
|
|
@ -0,0 +1,4 @@
|
|||
<!--
|
||||
Is this a bug report?
|
||||
If so, go back and select the "Bug report" option or your issue WILL be closed.
|
||||
--!>
|
|
@ -0,0 +1,6 @@
|
|||
requiredHeaders:
|
||||
- Prerequisites
|
||||
- Expected Behavior
|
||||
- Current Behavior
|
||||
- Possible Solution
|
||||
- Your Environment
|
|
@ -0,0 +1,17 @@
|
|||
# Number of days of inactivity before an issue becomes stale
|
||||
daysUntilStale: 60
|
||||
# Number of days of inactivity before a stale issue is closed
|
||||
daysUntilClose: 7
|
||||
# Issues with these labels will never be considered stale
|
||||
exemptLabels:
|
||||
- pr
|
||||
- discussion
|
||||
- e2e
|
||||
- enhancement
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed if no further activity occurs. Thank you
|
||||
for your contributions.
|
||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||
closeComment: false
|
|
@ -0,0 +1,84 @@
|
|||
name: Electron CD
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- release
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-16.04, macOS-latest, windows-latest]
|
||||
|
||||
steps:
|
||||
- name: Context
|
||||
env:
|
||||
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||
run: echo "$GITHUB_CONTEXT"
|
||||
- uses: actions/checkout@v1
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: 1.41.0
|
||||
override: true
|
||||
- name: Use Node.js 12.x
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12.x
|
||||
- name: yarn install
|
||||
run: |
|
||||
yarn install
|
||||
- name: Build and Package@Linux
|
||||
if: contains(matrix.os, 'ubuntu')
|
||||
run: |
|
||||
yarn package-linux
|
||||
- name: Build and Package@Mac
|
||||
if: contains(matrix.os, 'macos')
|
||||
run: |
|
||||
yarn package-mac
|
||||
- name: Build and Package@Win
|
||||
if: contains(matrix.os, 'windows')
|
||||
run: |
|
||||
yarn package-win
|
||||
- name: Version@Linux@Mac
|
||||
if: contains(matrix.os, 'ubuntu') || contains(matrix.os, 'macos')
|
||||
run: |
|
||||
./bin/printversion.sh
|
||||
- name: Version@Win
|
||||
if: contains(matrix.os, 'windows')
|
||||
run: |
|
||||
./bin/printversion.ps1
|
||||
- name: Upload artifacts-deb
|
||||
uses: actions/upload-artifact@v1
|
||||
if: contains(matrix.os, 'ubuntu')
|
||||
with:
|
||||
name: ${{ matrix.os }}
|
||||
path: release/Zecwallet_Lite_${{ env.VERSION }}_amd64.deb
|
||||
- name: Upload artifacts-AppImage
|
||||
uses: actions/upload-artifact@v1
|
||||
if: contains(matrix.os, 'ubuntu')
|
||||
with:
|
||||
name: ${{ matrix.os }}
|
||||
path: release/Zecwallet Lite-${{ env.VERSION }}.AppImage
|
||||
- name: Upload artifacts-dmg
|
||||
uses: actions/upload-artifact@v1
|
||||
if: contains(matrix.os, 'macos')
|
||||
with:
|
||||
name: ${{ matrix.os }}
|
||||
path: release/Zecwallet Lite-${{ env.VERSION }}.dmg
|
||||
- name: Upload artifacts-zip
|
||||
uses: actions/upload-artifact@v1
|
||||
if: contains(matrix.os, 'windows')
|
||||
with:
|
||||
name: ${{ matrix.os }}
|
||||
path: release/Zecwallet Lite-${{ env.VERSION }}-win.zip
|
||||
- name: Upload artifacts-msi
|
||||
uses: actions/upload-artifact@v1
|
||||
if: contains(matrix.os, 'windows')
|
||||
with:
|
||||
name: ${{ matrix.os }}
|
||||
path: release/Zecwallet Lite ${{ env.VERSION }}.msi
|
|
@ -1,42 +1,55 @@
|
|||
bin/
|
||||
debug/
|
||||
release/
|
||||
x64/
|
||||
artifacts/
|
||||
docs/website/public
|
||||
.vscode/
|
||||
res/libsodium.a
|
||||
res/libsodium/libsodium*
|
||||
res/libsodium.a
|
||||
src/ui_*.h
|
||||
*.autosave
|
||||
src/precompiled.h.cpp
|
||||
.qmake.stash
|
||||
zecwallet
|
||||
Zecwallet-Lite.app
|
||||
zecwallet-lite-mingw*
|
||||
zecwallet-lite.vcxproj*
|
||||
zecwallet.vcxproj*
|
||||
zecwallet-lite.sln
|
||||
zecwallet-lite.pro.user
|
||||
/Makefile
|
||||
/Makefile.*
|
||||
qrc_application.cpp
|
||||
zec-qt-wallet_plugin_import.cpp
|
||||
zecwallet_plugin_import.cpp
|
||||
zec-qt-wallet_resource*
|
||||
zecwallet_resource*
|
||||
workspace.code-workspace
|
||||
*.zip
|
||||
*.tar.gz
|
||||
*.xcsettings
|
||||
.DS_Store
|
||||
*.mak
|
||||
zcashd
|
||||
IDEWorkspaceChecks.plist
|
||||
*.sln
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
.eslintcache
|
||||
|
||||
# Dependency directory
|
||||
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
|
||||
node_modules
|
||||
zec-qt-wallet.pro.user.4.10-pre1
|
||||
zecwallet-lite
|
||||
zecwallet-lite_plugin_import.cpp
|
||||
zecwallet-lite_resource.rc
|
||||
|
||||
# OSX
|
||||
.DS_Store
|
||||
|
||||
# flow-typed
|
||||
flow-typed/npm/*
|
||||
!flow-typed/npm/module_vx.x.x.js
|
||||
|
||||
# App packaged
|
||||
release
|
||||
app/main.prod.js
|
||||
app/main.prod.js.map
|
||||
app/renderer.prod.js
|
||||
app/renderer.prod.js.map
|
||||
app/style.css
|
||||
app/style.css.map
|
||||
dist
|
||||
dll
|
||||
main.js
|
||||
main.js.map
|
||||
|
||||
.idea
|
||||
npm-debug.log.*
|
||||
|
||||
zcashd
|
||||
|
||||
native/artifacts.json
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"overrides": [
|
||||
{
|
||||
"files": [".prettierrc", ".babelrc", ".eslintrc", ".stylelintrc"],
|
||||
"options": {
|
||||
"parser": "json"
|
||||
}
|
||||
}
|
||||
],
|
||||
"singleQuote": true,
|
||||
"printWidth": 120
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"extends": ["stylelint-config-standard", "stylelint-config-prettier"]
|
||||
}
|
27
.travis.yml
|
@ -1,27 +0,0 @@
|
|||
matrix:
|
||||
include:
|
||||
# works on Precise and Trusty
|
||||
- os: linux
|
||||
compiler: clang
|
||||
addons:
|
||||
apt:
|
||||
sources: ['ubuntu-toolchain-r-test', 'llvm-toolchain-precise-3.7']
|
||||
packages: ['clang-3.7', 'g++-8']
|
||||
|
||||
before_install:
|
||||
- sudo add-apt-repository ppa:beineri/opt-qt591-xenial -y
|
||||
- sudo apt-get update -qq
|
||||
- sudo apt-get install qt59base qt59websockets libgl1-mesa-dev
|
||||
- source /opt/qt59/bin/qt59-env.sh
|
||||
|
||||
script:
|
||||
- qmake -v
|
||||
- clang++ -v
|
||||
- g++-8 -v
|
||||
- qmake zec-qt-wallet.pro CONFIG+=release -spec linux-clang
|
||||
- make CC=clang CXX=clang++ -j2
|
||||
- make distclean
|
||||
- qmake zec-qt-wallet.pro CONFIG+=release -spec linux-g++
|
||||
- res/libsodium/buildlibsodium.sh
|
||||
- make CC=gcc-8 CXX=g++-8 -j2
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"dzannotti.vscode-babel-coloring",
|
||||
"EditorConfig.EditorConfig",
|
||||
"flowtype.flow-for-vscode"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Electron: Renderer",
|
||||
"type": "chrome",
|
||||
"request": "attach",
|
||||
"port": 9223,
|
||||
"webRoot": "${workspaceFolder}",
|
||||
"timeout": 150000
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
"files.associations": {
|
||||
".babelrc": "jsonc",
|
||||
".eslintrc": "jsonc",
|
||||
".prettierrc": "jsonc",
|
||||
|
||||
".stylelintrc": "json",
|
||||
|
||||
".dockerignore": "ignore",
|
||||
".eslintignore": "ignore",
|
||||
".flowconfig": "ignore"
|
||||
},
|
||||
|
||||
"javascript.validate.enable": false,
|
||||
"javascript.format.enable": false,
|
||||
"typescript.validate.enable": false,
|
||||
"typescript.format.enable": false,
|
||||
|
||||
"flow.useNPMPackagedFlow": true,
|
||||
"search.exclude": {
|
||||
".git": true,
|
||||
".eslintcache": true,
|
||||
"app/dist": true,
|
||||
"app/main.prod.js": true,
|
||||
"app/main.prod.js.map": true,
|
||||
"bower_components": true,
|
||||
"dll": true,
|
||||
"flow-typed": true,
|
||||
"release": true,
|
||||
"node_modules": true,
|
||||
"npm-debug.log.*": true,
|
||||
"test/**/__snapshots__": true,
|
||||
"yarn.lock": true
|
||||
},
|
||||
"workbench.colorCustomizations": {
|
||||
"activityBar.background": "#49212F",
|
||||
"titleBar.activeBackground": "#662E42",
|
||||
"titleBar.activeForeground": "#FCF8FA"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
||||
// for the documentation about the tasks.json format
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "dev",
|
||||
"group": { "kind": "build", "isDefault": true },
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"label": "Start Webpack Dev",
|
||||
"script": "start-renderer-dev",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
"isBackground": true,
|
||||
"problemMatcher": {
|
||||
"owner": "custom",
|
||||
"pattern": {
|
||||
"regexp": "____________"
|
||||
},
|
||||
"background": {
|
||||
"activeOnStart": true,
|
||||
"beginsPattern": "Compiling\\.\\.\\.$",
|
||||
"endsPattern": "(Compiled successfully|Failed to compile)\\.$"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
23
LICENSE
|
@ -1,7 +1,22 @@
|
|||
Copyright 2018 adityapk
|
||||
The MIT License (MIT)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
Copyright (c) 2018-present Zecwallet
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@ -0,0 +1,35 @@
|
|||
/* eslint-disable camelcase */
|
||||
const fs = require('fs');
|
||||
const electron_notarize = require('electron-notarize');
|
||||
|
||||
module.exports = async function(params) {
|
||||
// Only notarize the app on Mac OS only.
|
||||
if (process.platform !== 'darwin') {
|
||||
return;
|
||||
}
|
||||
// console.log('afterSign hook triggered', params);
|
||||
|
||||
// Same appId in electron-builder.
|
||||
const appId = 'co.zecwallet.lite';
|
||||
|
||||
const appPath = params.artifactPaths.find(p => p.endsWith('.dmg'));
|
||||
|
||||
if (!fs.existsSync(appPath)) {
|
||||
throw new Error(`Cannot find application at: ${appPath}`);
|
||||
}
|
||||
|
||||
console.log(`Notarizing ${appId} found at ${appPath}`);
|
||||
|
||||
try {
|
||||
await electron_notarize.notarize({
|
||||
appBundleId: appId,
|
||||
appPath,
|
||||
appleId: process.env.appleId,
|
||||
appleIdPassword: process.env.appleIdPassword
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
console.log(`Done notarizing ${appId}`);
|
||||
};
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"mainWindowUrl": "./app.html",
|
||||
"appPath": "."
|
||||
}
|
|
@ -0,0 +1,548 @@
|
|||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
/* eslint-disable max-classes-per-file */
|
||||
/* eslint-disable react/prop-types */
|
||||
/* eslint-disable react/no-unused-state */
|
||||
import React from 'react';
|
||||
import ReactModal from 'react-modal';
|
||||
import { Switch, Route } from 'react-router';
|
||||
import native from '../native/index.node';
|
||||
import { ErrorModal, ErrorModalData } from './components/ErrorModal';
|
||||
import cstyles from './components/Common.css';
|
||||
import routes from './constants/routes.json';
|
||||
import App from './containers/App';
|
||||
import Dashboard from './components/Dashboard';
|
||||
import Send from './components/Send';
|
||||
import Receive from './components/Receive';
|
||||
import LoadingScreen from './components/LoadingScreen';
|
||||
import AppState, {
|
||||
AddressBalance,
|
||||
TotalBalance,
|
||||
Transaction,
|
||||
SendPageState,
|
||||
ToAddr,
|
||||
RPCConfig,
|
||||
Info,
|
||||
ReceivePageState,
|
||||
AddressBookEntry,
|
||||
PasswordState
|
||||
} from './components/AppState';
|
||||
import RPC from './rpc';
|
||||
import Utils from './utils/utils';
|
||||
import Zcashd from './components/Zcashd';
|
||||
import AddressBook from './components/Addressbook';
|
||||
import AddressbookImpl from './utils/AddressbookImpl';
|
||||
import Sidebar from './components/Sidebar';
|
||||
import Transactions from './components/Transactions';
|
||||
import PasswordModal from './components/PasswordModal';
|
||||
import CompanionAppListener from './companion';
|
||||
import WormholeConnection from './components/WormholeConnection';
|
||||
|
||||
type Props = {};
|
||||
|
||||
export default class RouteApp extends React.Component<Props, AppState> {
|
||||
rpc: RPC;
|
||||
|
||||
companionAppListener: CompanionAppListener;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
totalBalance: new TotalBalance(),
|
||||
addressesWithBalance: [],
|
||||
addressPrivateKeys: {},
|
||||
addresses: [],
|
||||
addressBook: [],
|
||||
transactions: null,
|
||||
sendPageState: new SendPageState(),
|
||||
receivePageState: new ReceivePageState(),
|
||||
rpcConfig: new RPCConfig(),
|
||||
info: new Info(),
|
||||
rescanning: false,
|
||||
location: null,
|
||||
errorModalData: new ErrorModalData(),
|
||||
passwordState: new PasswordState(),
|
||||
connectedCompanionApp: null
|
||||
};
|
||||
|
||||
// Create the initial ToAddr box
|
||||
// eslint-disable-next-line react/destructuring-assignment
|
||||
this.state.sendPageState.toaddrs = [new ToAddr(Utils.getNextToAddrID())];
|
||||
|
||||
// Set the Modal's app element
|
||||
ReactModal.setAppElement('#root');
|
||||
|
||||
console.log(native.litelib_wallet_exists('main'));
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (!this.rpc) {
|
||||
this.rpc = new RPC(
|
||||
this.setTotalBalance,
|
||||
this.setAddressesWithBalances,
|
||||
this.setTransactionList,
|
||||
this.setAllAddresses,
|
||||
this.setInfo,
|
||||
this.setZecPrice
|
||||
);
|
||||
}
|
||||
|
||||
// Read the address book
|
||||
(async () => {
|
||||
const addressBook = await AddressbookImpl.readAddressBook();
|
||||
if (addressBook) {
|
||||
this.setState({ addressBook });
|
||||
}
|
||||
})();
|
||||
|
||||
// Setup the websocket for the companion app
|
||||
this.companionAppListener = new CompanionAppListener(
|
||||
this.getFullState,
|
||||
this.sendTransaction,
|
||||
this.updateConnectedCompanionApp
|
||||
);
|
||||
this.companionAppListener.setUp();
|
||||
}
|
||||
|
||||
componentWillUnmount() {}
|
||||
|
||||
getFullState = (): AppState => {
|
||||
return this.state;
|
||||
};
|
||||
|
||||
openErrorModal = (title: string, body: string) => {
|
||||
const errorModalData = new ErrorModalData();
|
||||
errorModalData.modalIsOpen = true;
|
||||
errorModalData.title = title;
|
||||
errorModalData.body = body;
|
||||
|
||||
this.setState({ errorModalData });
|
||||
};
|
||||
|
||||
closeErrorModal = () => {
|
||||
const errorModalData = new ErrorModalData();
|
||||
errorModalData.modalIsOpen = false;
|
||||
|
||||
this.setState({ errorModalData });
|
||||
};
|
||||
|
||||
openPassword = (
|
||||
confirmNeeded: boolean,
|
||||
passwordCallback: string => void,
|
||||
closeCallback: () => void,
|
||||
helpText: string
|
||||
) => {
|
||||
const passwordState = new PasswordState();
|
||||
|
||||
passwordState.showPassword = true;
|
||||
passwordState.confirmNeeded = confirmNeeded;
|
||||
passwordState.helpText = helpText;
|
||||
|
||||
// Set the callbacks, but before calling them back, we close the modals
|
||||
passwordState.passwordCallback = (password: string) => {
|
||||
this.setState({ passwordState: new PasswordState() });
|
||||
|
||||
// Call the callback after a bit, so as to give time to the modal to close
|
||||
setTimeout(() => passwordCallback(password), 10);
|
||||
};
|
||||
passwordState.closeCallback = () => {
|
||||
this.setState({ passwordState: new PasswordState() });
|
||||
|
||||
// Call the callback after a bit, so as to give time to the modal to close
|
||||
setTimeout(() => closeCallback(), 10);
|
||||
};
|
||||
|
||||
this.setState({ passwordState });
|
||||
};
|
||||
|
||||
// This will:
|
||||
// 1. Check if the wallet is encrypted and locked
|
||||
// 2. If it is, open the password dialog
|
||||
// 3. Attempt to unlock wallet.
|
||||
// a. If unlock suceeds, do the callback
|
||||
// b. If the unlock fails, show an error
|
||||
// 4. If wallet is not encrypted or already unlocked, just call the successcallback.
|
||||
openPasswordAndUnlockIfNeeded = (successCallback: () => void) => {
|
||||
// Check if it is locked
|
||||
const { info } = this.state;
|
||||
|
||||
if (info.encrypted && info.locked) {
|
||||
this.openPassword(
|
||||
false,
|
||||
(password: string) => {
|
||||
(async () => {
|
||||
const success = await this.unlockWallet(password);
|
||||
|
||||
if (success) {
|
||||
// If the unlock succeeded, do the submit
|
||||
successCallback();
|
||||
} else {
|
||||
this.openErrorModal('Wallet unlock failed', 'Could not unlock the wallet with the password.');
|
||||
}
|
||||
})();
|
||||
},
|
||||
// Close callback is a no-op
|
||||
() => {}
|
||||
);
|
||||
} else {
|
||||
successCallback();
|
||||
}
|
||||
};
|
||||
|
||||
unlockWallet = async (password: string): boolean => {
|
||||
const success = await this.rpc.unlockWallet(password);
|
||||
|
||||
return success;
|
||||
};
|
||||
|
||||
lockWallet = async (): boolean => {
|
||||
const success = await this.rpc.lockWallet();
|
||||
return success;
|
||||
};
|
||||
|
||||
encryptWallet = async (password): boolean => {
|
||||
const success = await this.rpc.encryptWallet(password);
|
||||
return success;
|
||||
};
|
||||
|
||||
decryptWallet = async (password): boolean => {
|
||||
const success = await this.rpc.decryptWallet(password);
|
||||
return success;
|
||||
};
|
||||
|
||||
setInfo = (info: Info) => {
|
||||
this.setState({ info });
|
||||
};
|
||||
|
||||
setTotalBalance = (totalBalance: TotalBalance) => {
|
||||
this.setState({ totalBalance });
|
||||
};
|
||||
|
||||
setAddressesWithBalances = (addressesWithBalance: AddressBalance[]) => {
|
||||
this.setState({ addressesWithBalance });
|
||||
|
||||
const { sendPageState } = this.state;
|
||||
|
||||
// If there is no 'from' address, we'll set a default one
|
||||
if (!sendPageState.fromaddr) {
|
||||
// Find a z-address with the highest balance
|
||||
const defaultAB = addressesWithBalance
|
||||
.filter(ab => Utils.isSapling(ab.address))
|
||||
.reduce((prev, ab) => {
|
||||
// We'll start with a sapling address
|
||||
if (prev == null) {
|
||||
return ab;
|
||||
}
|
||||
// Find the sapling address with the highest balance
|
||||
if (prev.balance < ab.balance) {
|
||||
return ab;
|
||||
}
|
||||
|
||||
return prev;
|
||||
}, null);
|
||||
|
||||
if (defaultAB) {
|
||||
const newSendPageState = new SendPageState();
|
||||
newSendPageState.fromaddr = defaultAB.address;
|
||||
newSendPageState.toaddrs = sendPageState.toaddrs;
|
||||
|
||||
this.setState({ sendPageState: newSendPageState });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
setTransactionList = (transactions: Transaction[]) => {
|
||||
this.setState({ transactions });
|
||||
};
|
||||
|
||||
setAllAddresses = (addresses: string[]) => {
|
||||
this.setState({ addresses });
|
||||
};
|
||||
|
||||
setSendPageState = (sendPageState: SendPageState) => {
|
||||
this.setState({ sendPageState });
|
||||
};
|
||||
|
||||
setSendTo = (address: string, amount: number | null, memo: string | null) => {
|
||||
// Clear the existing send page state and set up the new one
|
||||
const { sendPageState } = this.state;
|
||||
|
||||
const newSendPageState = new SendPageState();
|
||||
newSendPageState.fromaddr = sendPageState.fromaddr;
|
||||
|
||||
const to = new ToAddr(Utils.getNextToAddrID());
|
||||
if (address) {
|
||||
to.to = address;
|
||||
}
|
||||
if (amount) {
|
||||
to.amount = amount;
|
||||
}
|
||||
if (memo) {
|
||||
to.memo = memo;
|
||||
}
|
||||
newSendPageState.toaddrs = [to];
|
||||
|
||||
this.setState({ sendPageState: newSendPageState });
|
||||
};
|
||||
|
||||
setRPCConfig = (rpcConfig: RPCConfig) => {
|
||||
this.setState({ rpcConfig });
|
||||
console.log(rpcConfig);
|
||||
this.rpc.configure(rpcConfig);
|
||||
};
|
||||
|
||||
setZecPrice = (price: number | null) => {
|
||||
console.log(`Price = ${price}`);
|
||||
const { info } = this.state;
|
||||
|
||||
const newInfo = new Info();
|
||||
Object.assign(newInfo, info);
|
||||
newInfo.zecPrice = price;
|
||||
|
||||
this.setState({ info: newInfo });
|
||||
};
|
||||
|
||||
setRescanning = (rescanning: boolean) => {
|
||||
this.setState({ rescanning });
|
||||
};
|
||||
|
||||
setInfo = (newInfo: Info) => {
|
||||
// If the price is not set in this object, copy it over from the current object
|
||||
const { info } = this.state;
|
||||
if (!newInfo.zecPrice) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
newInfo.zecPrice = info.zecPrice;
|
||||
}
|
||||
|
||||
this.setState({ info: newInfo });
|
||||
};
|
||||
|
||||
sendTransaction = (sendJson: []): string => {
|
||||
try {
|
||||
const txid = this.rpc.sendTransaction(sendJson);
|
||||
return txid;
|
||||
} catch (err) {
|
||||
console.log('route sendtx error', err);
|
||||
}
|
||||
};
|
||||
|
||||
// Get a single private key for this address, and return it as a string.
|
||||
// Wallet needs to be unlocked
|
||||
getPrivKeyAsString = (address: string): string => {
|
||||
const pk = RPC.getPrivKeyAsString(address);
|
||||
return pk;
|
||||
};
|
||||
|
||||
// Getter methods, which are called by the components to update the state
|
||||
fetchAndSetSinglePrivKey = async (address: string) => {
|
||||
this.openPasswordAndUnlockIfNeeded(async () => {
|
||||
const key = await RPC.getPrivKeyAsString(address);
|
||||
const addressPrivateKeys = {};
|
||||
addressPrivateKeys[address] = key;
|
||||
|
||||
this.setState({ addressPrivateKeys });
|
||||
});
|
||||
};
|
||||
|
||||
addAddressBookEntry = (label: string, address: string) => {
|
||||
// Add an entry into the address book
|
||||
const { addressBook } = this.state;
|
||||
const newAddressBook = addressBook.concat(new AddressBookEntry(label, address));
|
||||
|
||||
// Write to disk. This method is async
|
||||
AddressbookImpl.writeAddressBook(newAddressBook);
|
||||
|
||||
this.setState({ addressBook: newAddressBook });
|
||||
};
|
||||
|
||||
removeAddressBookEntry = (label: string) => {
|
||||
const { addressBook } = this.state;
|
||||
const newAddressBook = addressBook.filter(i => i.label !== label);
|
||||
|
||||
// Write to disk. This method is async
|
||||
AddressbookImpl.writeAddressBook(newAddressBook);
|
||||
|
||||
this.setState({ addressBook: newAddressBook });
|
||||
};
|
||||
|
||||
createNewAddress = async (zaddress: boolean) => {
|
||||
// Create a new address
|
||||
const newaddress = RPC.createNewAddress(zaddress);
|
||||
console.log(`Created new Address ${newaddress}`);
|
||||
|
||||
// And then fetch the list of addresses again to refresh (totalBalance gets all addresses)
|
||||
this.rpc.fetchTotalBalance();
|
||||
|
||||
const { receivePageState } = this.state;
|
||||
const newRerenderKey = receivePageState.rerenderKey + 1;
|
||||
|
||||
const newReceivePageState = new ReceivePageState();
|
||||
newReceivePageState.newAddress = newaddress;
|
||||
newReceivePageState.rerenderKey = newRerenderKey;
|
||||
|
||||
this.setState({ receivePageState: newReceivePageState });
|
||||
};
|
||||
|
||||
updateConnectedCompanionApp = (connectedCompanionApp: ConnectedCompanionApp | null) => {
|
||||
this.setState({ connectedCompanionApp });
|
||||
};
|
||||
|
||||
doRefresh = () => {
|
||||
this.rpc.refresh();
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
totalBalance,
|
||||
transactions,
|
||||
addressesWithBalance,
|
||||
addressPrivateKeys,
|
||||
addresses,
|
||||
addressBook,
|
||||
sendPageState,
|
||||
receivePageState,
|
||||
rpcConfig,
|
||||
info,
|
||||
rescanning,
|
||||
errorModalData,
|
||||
passwordState,
|
||||
connectedCompanionApp
|
||||
} = this.state;
|
||||
|
||||
const standardProps = {
|
||||
openErrorModal: this.openErrorModal,
|
||||
closeErrorModal: this.closeErrorModal,
|
||||
setSendTo: this.setSendTo,
|
||||
info,
|
||||
openPasswordAndUnlockIfNeeded: this.openPasswordAndUnlockIfNeeded
|
||||
};
|
||||
|
||||
return (
|
||||
<App>
|
||||
<ErrorModal
|
||||
title={errorModalData.title}
|
||||
body={errorModalData.body}
|
||||
modalIsOpen={errorModalData.modalIsOpen}
|
||||
closeModal={this.closeErrorModal}
|
||||
/>
|
||||
|
||||
<PasswordModal
|
||||
modalIsOpen={passwordState.showPassword}
|
||||
confirmNeeded={passwordState.confirmNeeded}
|
||||
passwordCallback={passwordState.passwordCallback}
|
||||
closeCallback={passwordState.closeCallback}
|
||||
helpText={passwordState.helpText}
|
||||
/>
|
||||
|
||||
<div style={{ overflow: 'hidden' }}>
|
||||
{info && info.version && (
|
||||
<div className={cstyles.sidebarcontainer}>
|
||||
<Sidebar
|
||||
info={info}
|
||||
setInfo={this.setInfo}
|
||||
setSendTo={this.setSendTo}
|
||||
setRescanning={this.setRescanning}
|
||||
getPrivKeyAsString={this.getPrivKeyAsString}
|
||||
addresses={addresses}
|
||||
lockWallet={this.lockWallet}
|
||||
encryptWallet={this.encryptWallet}
|
||||
decryptWallet={this.decryptWallet}
|
||||
openPassword={this.openPassword}
|
||||
{...standardProps}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className={cstyles.contentcontainer}>
|
||||
<Switch>
|
||||
<Route
|
||||
path={routes.SEND}
|
||||
render={() => (
|
||||
<Send
|
||||
addresses={addresses}
|
||||
sendTransaction={this.sendTransaction}
|
||||
sendPageState={sendPageState}
|
||||
setSendPageState={this.setSendPageState}
|
||||
totalBalance={totalBalance}
|
||||
addressBook={addressBook}
|
||||
{...standardProps}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
path={routes.RECEIVE}
|
||||
render={() => (
|
||||
<Receive
|
||||
rerenderKey={receivePageState.rerenderKey}
|
||||
addresses={addresses}
|
||||
addressesWithBalance={addressesWithBalance}
|
||||
addressPrivateKeys={addressPrivateKeys}
|
||||
receivePageState={receivePageState}
|
||||
addressBook={addressBook}
|
||||
{...standardProps}
|
||||
fetchAndSetSinglePrivKey={this.fetchAndSetSinglePrivKey}
|
||||
createNewAddress={this.createNewAddress}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
path={routes.ADDRESSBOOK}
|
||||
render={() => (
|
||||
<AddressBook
|
||||
addressBook={addressBook}
|
||||
addAddressBookEntry={this.addAddressBookEntry}
|
||||
removeAddressBookEntry={this.removeAddressBookEntry}
|
||||
{...standardProps}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
path={routes.DASHBOARD}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
render={() => (
|
||||
<Dashboard totalBalance={totalBalance} info={info} addressesWithBalance={addressesWithBalance} />
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
path={routes.TRANSACTIONS}
|
||||
render={() => (
|
||||
<Transactions
|
||||
transactions={transactions}
|
||||
info={info}
|
||||
addressBook={addressBook}
|
||||
setSendTo={this.setSendTo}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path={routes.ZCASHD}
|
||||
render={() => <Zcashd info={info} rpcConfig={rpcConfig} refresh={this.doRefresh} />}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path={routes.CONNECTMOBILE}
|
||||
render={() => (
|
||||
<WormholeConnection
|
||||
companionAppListener={this.companionAppListener}
|
||||
connectedCompanionApp={connectedCompanionApp}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
path={routes.LOADING}
|
||||
render={() => (
|
||||
<LoadingScreen
|
||||
setRPCConfig={this.setRPCConfig}
|
||||
rescanning={rescanning}
|
||||
setRescanning={this.setRescanning}
|
||||
setInfo={this.setInfo}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
</App>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,144 @@
|
|||
/*
|
||||
* @NOTE: Prepend a `~` to css file paths that are in your node_modules
|
||||
* See https://github.com/webpack-contrib/sass-loader#imports
|
||||
*/
|
||||
@import '~@fortawesome/fontawesome-free/css/all.css';
|
||||
@import '~typeface-roboto/index.css';
|
||||
|
||||
html {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
position: relative;
|
||||
color: white;
|
||||
height: 100vh;
|
||||
background-color: #212124;
|
||||
font-family: Roboto, Arial, Helvetica, Helvetica Neue, serif;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 0.5em;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
box-shadow: inset 0 0 6px grey;
|
||||
background-color: rgb(1, 1, 1);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: white;
|
||||
outline: 1px solid slategrey;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 2.25rem;
|
||||
font-weight: bold;
|
||||
letter-spacing: -0.025em;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
li {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
a {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
input[disabled] {
|
||||
color: grey;
|
||||
}
|
||||
|
||||
.react-tabs {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.react-tabs__tab-list {
|
||||
border-bottom: 1px solid #aaa;
|
||||
margin: 0 0 10px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.react-tabs__tab {
|
||||
display: inline-block;
|
||||
border: 1px solid transparent;
|
||||
border-bottom: none;
|
||||
bottom: -1px;
|
||||
position: relative;
|
||||
list-style: none;
|
||||
padding: 6px 12px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.react-tabs__tab--selected {
|
||||
background: #c3921f;
|
||||
color: black;
|
||||
border-radius: 5px 5px 0 0;
|
||||
}
|
||||
|
||||
.react-tabs__tab--disabled {
|
||||
color: grey;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.react-tabs__tab:focus {
|
||||
box-shadow: 0 0 5px hsl(208, 99%, 50%);
|
||||
border-color: hsl(208, 99%, 50%);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.react-tabs__tab:focus::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
height: 5px;
|
||||
left: -4px;
|
||||
right: -4px;
|
||||
bottom: -5px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.react-tabs__tab-panel {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.react-tabs__tab-panel--selected {
|
||||
display: block;
|
||||
}
|
||||
|
||||
img,
|
||||
img::after,
|
||||
img::before {
|
||||
-webkit-user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
-webkit-app-region: no-drag;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
a,
|
||||
a::after,
|
||||
a::before {
|
||||
-webkit-user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
-webkit-app-region: no-drag;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
opacity: 0.8;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input[type='number']::-webkit-inner-spin-button,
|
||||
input[type='number']::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Zecwallet Lite</title>
|
||||
|
||||
<!-- Allow unsafe only for inline script tags, no external content-->
|
||||
<script nonce="1u1na">
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
document.write(
|
||||
`<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline'; style-src 'self'; " >`
|
||||
);
|
||||
}
|
||||
|
||||
(() => {
|
||||
if (!process.env.START_HOT) {
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = './dist/style.css';
|
||||
// HACK: Writing the script path should be done with webpack
|
||||
document.getElementsByTagName('head')[0].appendChild(link);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script nonce="8SHwb">
|
||||
{
|
||||
const scripts = [];
|
||||
|
||||
// Dynamically insert the DLL script in development env in the
|
||||
// renderer process
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
scripts.push('../dll/renderer.dev.dll.js');
|
||||
}
|
||||
|
||||
// Dynamically insert the bundled app script in the renderer process
|
||||
const port = process.env.PORT || 1212;
|
||||
scripts.push(
|
||||
process.env.START_HOT ? `http://localhost:${port}/dist/renderer.dev.js` : './dist/renderer.prod.js'
|
||||
);
|
||||
|
||||
document.write(scripts.map(script => `<script defer src="${script}"><\/script>`).join(''));
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
After Width: | Height: | Size: 747 B |
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 8.0 KiB |
After Width: | Height: | Size: 5.3 KiB |
After Width: | Height: | Size: 4.1 KiB |
|
@ -0,0 +1,499 @@
|
|||
/* eslint-disable flowtype/no-weak-types */
|
||||
/* eslint-disable max-classes-per-file */
|
||||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import _sodium from 'libsodium-wrappers-sumo';
|
||||
import Store from 'electron-store';
|
||||
import WebSocket from 'ws';
|
||||
import AppState, { ConnectedCompanionApp } from './components/AppState';
|
||||
import Utils from './utils/utils';
|
||||
|
||||
// Wormhole code is sha256(sha256(secret_key))
|
||||
function getWormholeCode(keyHex: string, sodium: any): string {
|
||||
const key = sodium.from_hex(keyHex);
|
||||
|
||||
const pass1 = sodium.crypto_hash_sha256(key);
|
||||
const pass2 = sodium.to_hex(sodium.crypto_hash_sha256(pass1));
|
||||
|
||||
return pass2;
|
||||
}
|
||||
|
||||
// A class that connects to wormhole given a secret key
|
||||
class WormholeClient {
|
||||
keyHex: string;
|
||||
|
||||
wormholeCode: string;
|
||||
|
||||
sodium: any;
|
||||
|
||||
wss: WebSocket = null;
|
||||
|
||||
listner: CompanionAppListener = null;
|
||||
|
||||
keepAliveTimerID: TimerID = null;
|
||||
|
||||
constructor(keyHex: string, sodium: any, listner: CompanionAppListener) {
|
||||
this.keyHex = keyHex;
|
||||
this.sodium = sodium;
|
||||
this.listner = listner;
|
||||
|
||||
this.wormholeCode = getWormholeCode(keyHex, this.sodium);
|
||||
|
||||
this.connect();
|
||||
}
|
||||
|
||||
connect() {
|
||||
this.wss = new WebSocket('wss://wormhole.zecqtwallet.com:443');
|
||||
|
||||
this.wss.on('open', () => {
|
||||
// On open, register ourself
|
||||
const reg = { register: getWormholeCode(this.keyHex, this.sodium) };
|
||||
|
||||
// No encryption for the register call
|
||||
this.wss.send(JSON.stringify(reg));
|
||||
|
||||
// Now, do a ping every 4 minutes to keep the connection alive.
|
||||
this.keepAliveTimerID = setInterval(() => {
|
||||
const ping = { ping: 'ping' };
|
||||
this.wss.send(JSON.stringify(ping));
|
||||
}, 4 * 60 * 1000);
|
||||
});
|
||||
|
||||
this.wss.on('message', data => {
|
||||
this.listner.processIncoming(data, this.keyHex, this.wss);
|
||||
});
|
||||
|
||||
this.wss.on('close', (code, reason) => {
|
||||
console.log('Socket closed for ', this.keyHex, code, reason);
|
||||
});
|
||||
|
||||
this.wss.on('error', (ws, err) => {
|
||||
console.log('ws error', err);
|
||||
});
|
||||
}
|
||||
|
||||
getKeyHex(): string {
|
||||
return this.keyHex;
|
||||
}
|
||||
|
||||
close() {
|
||||
if (this.keepAliveTimerID) {
|
||||
clearInterval(this.keepAliveTimerID);
|
||||
}
|
||||
|
||||
// Close the websocket.
|
||||
if (this.wss) {
|
||||
this.wss.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The singleton Companion App listener, that can spawn a wormhole server
|
||||
// or (multiple) wormhole clients
|
||||
export default class CompanionAppListener {
|
||||
sodium = null;
|
||||
|
||||
fnGetState: () => AppState = null;
|
||||
|
||||
fnSendTransaction: ([]) => string = null;
|
||||
|
||||
fnUpdateConnectedClient: (string, number) => void = null;
|
||||
|
||||
permWormholeClient: WormholeClient = null;
|
||||
|
||||
tmpWormholeClient: WormholeClient = null;
|
||||
|
||||
constructor(
|
||||
fnGetSate: () => AppState,
|
||||
fnSendTransaction: ([]) => string,
|
||||
fnUpdateConnectedClient: (string, number) => void
|
||||
) {
|
||||
this.fnGetState = fnGetSate;
|
||||
this.fnSendTransaction = fnSendTransaction;
|
||||
this.fnUpdateConnectedClient = fnUpdateConnectedClient;
|
||||
}
|
||||
|
||||
async setUp() {
|
||||
await _sodium.ready;
|
||||
this.sodium = _sodium;
|
||||
|
||||
// Create a new wormhole listner
|
||||
const permKeyHex = this.getEncKey();
|
||||
if (permKeyHex) {
|
||||
this.permWormholeClient = new WormholeClient(permKeyHex, this.sodium, this);
|
||||
}
|
||||
|
||||
// At startup, set the last client name/time by loading it
|
||||
const store = new Store();
|
||||
const name = store.get('companion/name');
|
||||
const lastSeen = store.get('companion/lastseen');
|
||||
|
||||
if (name && lastSeen) {
|
||||
const o = new ConnectedCompanionApp();
|
||||
o.name = name;
|
||||
o.lastSeen = lastSeen;
|
||||
|
||||
this.fnUpdateConnectedClient(o);
|
||||
}
|
||||
}
|
||||
|
||||
createTmpClient(keyHex: string) {
|
||||
if (this.tmpWormholeClient) {
|
||||
this.tmpWormholeClient.close();
|
||||
}
|
||||
|
||||
this.tmpWormholeClient = new WormholeClient(keyHex, this.sodium, this);
|
||||
}
|
||||
|
||||
closeTmpClient() {
|
||||
if (this.tmpWormholeClient) {
|
||||
this.tmpWormholeClient.close();
|
||||
|
||||
this.tmpWormholeClient = null;
|
||||
}
|
||||
}
|
||||
|
||||
replacePermClientWithTmp() {
|
||||
if (this.permWormholeClient) {
|
||||
this.permWormholeClient.close();
|
||||
}
|
||||
|
||||
// Replace the stored code with the new one
|
||||
this.permWormholeClient = this.tmpWormholeClient;
|
||||
this.tmpWormholeClient = null;
|
||||
this.setEncKey(this.permWormholeClient.getKeyHex());
|
||||
|
||||
// Reset local nonce
|
||||
const store = new Store();
|
||||
store.delete('companion/localnonce');
|
||||
}
|
||||
|
||||
processIncoming(data: string, keyHex: string, ws: Websocket) {
|
||||
const dataJson = JSON.parse(data);
|
||||
|
||||
// If the wormhole sends some messages, we ignore them
|
||||
if ('error' in dataJson) {
|
||||
console.log('Incoming data contains an error message', data);
|
||||
return;
|
||||
}
|
||||
|
||||
// If the message is a ping, just ignore it
|
||||
if ('ping' in dataJson) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Then, check if the message is encrpted
|
||||
if (!('nonce' in dataJson)) {
|
||||
const err = { error: 'Encryption error', to: getWormholeCode(keyHex, this.sodium) };
|
||||
ws.send(JSON.stringify(err));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let cmd;
|
||||
|
||||
// If decryption passes and this is a tmp wormhole client, then set it as the permanant client
|
||||
if (this.tmpWormholeClient && keyHex === this.tmpWormholeClient.getKeyHex()) {
|
||||
const { decrypted, nonce } = this.decryptIncoming(data, keyHex, false);
|
||||
if (!decrypted) {
|
||||
console.log('Decryption failed');
|
||||
|
||||
const err = { error: 'Encryption error', to: getWormholeCode(keyHex, this.sodium) };
|
||||
ws.send(JSON.stringify(err));
|
||||
return;
|
||||
}
|
||||
cmd = JSON.parse(decrypted);
|
||||
|
||||
// Replace the permanant client
|
||||
this.replacePermClientWithTmp();
|
||||
|
||||
this.updateRemoteNonce(nonce);
|
||||
} else {
|
||||
const { decrypted, nonce } = this.decryptIncoming(data, keyHex, true);
|
||||
if (!decrypted) {
|
||||
const err = { error: 'Encryption error', to: getWormholeCode(keyHex, this.sodium) };
|
||||
ws.send(JSON.stringify(err));
|
||||
|
||||
console.log('Decryption failed');
|
||||
return;
|
||||
}
|
||||
|
||||
cmd = JSON.parse(decrypted);
|
||||
|
||||
this.updateRemoteNonce(nonce);
|
||||
}
|
||||
|
||||
if (cmd.command === 'getInfo') {
|
||||
const response = this.doGetInfo(cmd);
|
||||
ws.send(this.encryptOutgoing(response, keyHex));
|
||||
} else if (cmd.command === 'getTransactions') {
|
||||
const response = this.doGetTransactions();
|
||||
ws.send(this.encryptOutgoing(response, keyHex));
|
||||
} else if (cmd.command === 'sendTx') {
|
||||
const response = this.doSendTransaction(cmd, ws);
|
||||
ws.send(this.encryptOutgoing(response, keyHex));
|
||||
}
|
||||
}
|
||||
|
||||
// Generate a new secret key
|
||||
genNewKeyHex(): string {
|
||||
const keyHex = this.sodium.to_hex(this.sodium.crypto_secretbox_keygen());
|
||||
|
||||
return keyHex;
|
||||
}
|
||||
|
||||
getEncKey(): string {
|
||||
// Get the nonce. Increment and store the nonce for next use
|
||||
const store = new Store();
|
||||
const keyHex = store.get('companion/key');
|
||||
|
||||
return keyHex;
|
||||
}
|
||||
|
||||
setEncKey(keyHex: string) {
|
||||
// Get the nonce. Increment and store the nonce for next use
|
||||
const store = new Store();
|
||||
store.set('companion/key', keyHex);
|
||||
}
|
||||
|
||||
saveLastClientName(name: string) {
|
||||
// Save the last client name
|
||||
const store = new Store();
|
||||
store.set('companion/name', name);
|
||||
|
||||
if (name) {
|
||||
const now = Date.now();
|
||||
store.set('companion/lastseen', now);
|
||||
|
||||
const o = new ConnectedCompanionApp();
|
||||
o.name = name;
|
||||
o.lastSeen = now;
|
||||
|
||||
this.fnUpdateConnectedClient(o);
|
||||
} else {
|
||||
this.fnUpdateConnectedClient(null);
|
||||
}
|
||||
}
|
||||
|
||||
disconnectLastClient() {
|
||||
// Remove the permanant connection
|
||||
if (this.permWormholeClient) {
|
||||
this.permWormholeClient.close();
|
||||
}
|
||||
|
||||
this.saveLastClientName(null);
|
||||
this.setEncKey(null);
|
||||
}
|
||||
|
||||
getRemoteNonce(): string {
|
||||
const store = new Store();
|
||||
const nonceHex = store.get('companion/remotenonce');
|
||||
|
||||
return nonceHex;
|
||||
}
|
||||
|
||||
updateRemoteNonce(nonce: string) {
|
||||
if (nonce) {
|
||||
const store = new Store();
|
||||
store.set('companion/remotenonce', nonce);
|
||||
}
|
||||
}
|
||||
|
||||
getLocalNonce(): string {
|
||||
// Get the nonce. Increment and store the nonce for next use
|
||||
const store = new Store();
|
||||
const nonceHex = store.get('companion/localnonce', `01${'00'.repeat(this.sodium.crypto_secretbox_NONCEBYTES - 1)}`);
|
||||
|
||||
// Increment nonce
|
||||
const newNonce = this.sodium.from_hex(nonceHex);
|
||||
|
||||
this.sodium.increment(newNonce);
|
||||
this.sodium.increment(newNonce);
|
||||
store.set('companion/localnonce', this.sodium.to_hex(newNonce));
|
||||
|
||||
return nonceHex;
|
||||
}
|
||||
|
||||
encryptOutgoing(str: string, keyHex: string): string {
|
||||
if (!keyHex) {
|
||||
console.log('No secret key');
|
||||
throw Error('No Secret Key');
|
||||
}
|
||||
|
||||
const nonceHex = this.getLocalNonce();
|
||||
|
||||
const nonce = this.sodium.from_hex(nonceHex);
|
||||
const key = this.sodium.from_hex(keyHex);
|
||||
|
||||
const encrypted = this.sodium.crypto_secretbox_easy(str, nonce, key);
|
||||
const encryptedHex = this.sodium.to_hex(encrypted);
|
||||
|
||||
const resp = {
|
||||
nonce: this.sodium.to_hex(nonce),
|
||||
payload: encryptedHex,
|
||||
to: getWormholeCode(keyHex, this.sodium)
|
||||
};
|
||||
|
||||
return JSON.stringify(resp);
|
||||
}
|
||||
|
||||
decryptIncoming(msg: string, keyHex: string, checkNonce: boolean): any {
|
||||
const msgJson = JSON.parse(msg);
|
||||
console.log('trying to decrypt', msgJson);
|
||||
|
||||
if (!keyHex) {
|
||||
console.log('No secret key');
|
||||
throw Error('No Secret Key');
|
||||
}
|
||||
|
||||
const key = this.sodium.from_hex(keyHex);
|
||||
const nonce = this.sodium.from_hex(msgJson.nonce);
|
||||
|
||||
if (checkNonce) {
|
||||
const prevNonce = this.sodium.from_hex(this.getRemoteNonce());
|
||||
if (prevNonce && this.sodium.compare(prevNonce, nonce) >= 0) {
|
||||
return { decrypted: null };
|
||||
}
|
||||
}
|
||||
|
||||
const cipherText = this.sodium.from_hex(msgJson.payload);
|
||||
|
||||
const decrypted = this.sodium.to_string(this.sodium.crypto_secretbox_open_easy(cipherText, nonce, key));
|
||||
|
||||
return { decrypted, nonce: msgJson.nonce };
|
||||
}
|
||||
|
||||
doGetInfo(cmd: any): string {
|
||||
const appState = this.fnGetState();
|
||||
|
||||
if (cmd && cmd.name) {
|
||||
this.saveLastClientName(cmd.name);
|
||||
}
|
||||
|
||||
const saplingAddress = appState.addresses.find(a => Utils.isSapling(a));
|
||||
const tAddress = appState.addresses.find(a => Utils.isTransparent(a));
|
||||
const balance = parseFloat(appState.totalBalance.total);
|
||||
const maxspendable = parseFloat(appState.totalBalance.total);
|
||||
const maxzspendable = parseFloat(appState.totalBalance.private);
|
||||
const tokenName = appState.info.currencyName;
|
||||
const zecprice = parseFloat(appState.info.zecPrice);
|
||||
|
||||
const resp = {
|
||||
version: 1.0,
|
||||
command: 'getInfo',
|
||||
saplingAddress,
|
||||
tAddress,
|
||||
balance,
|
||||
maxspendable,
|
||||
maxzspendable,
|
||||
tokenName,
|
||||
zecprice,
|
||||
serverversion: '0.9.2'
|
||||
};
|
||||
|
||||
return JSON.stringify(resp);
|
||||
}
|
||||
|
||||
doGetTransactions(): string {
|
||||
const appState = this.fnGetState();
|
||||
|
||||
let txlist = [];
|
||||
if (appState.transactions) {
|
||||
// Get only the last 20 txns
|
||||
txlist = appState.transactions.slice(0, 20).map(t => {
|
||||
let memo = t.detailedTxns && t.detailedTxns.length > 0 ? t.detailedTxns[0].memo : '';
|
||||
if (memo) {
|
||||
memo = memo.trimRight();
|
||||
} else {
|
||||
memo = '';
|
||||
}
|
||||
|
||||
const txResp = {
|
||||
type: t.type,
|
||||
datetime: t.time,
|
||||
amount: t.amount.toFixed(8),
|
||||
txid: t.txid,
|
||||
address: t.address,
|
||||
memo,
|
||||
confirmations: t.confirmations
|
||||
};
|
||||
|
||||
return txResp;
|
||||
});
|
||||
}
|
||||
|
||||
const resp = {
|
||||
version: 1.0,
|
||||
command: 'getTransactions',
|
||||
transactions: txlist
|
||||
};
|
||||
|
||||
return JSON.stringify(resp);
|
||||
}
|
||||
|
||||
doSendTransaction(cmd: any, ws: WebSocket): string {
|
||||
// "command":"sendTx","tx":{"amount":"0.00019927","to":"zs1pzr7ee53jwa3h3yvzdjf7meruujq84w5rsr5kuvye9qg552kdyz5cs5ywy5hxkxcfvy9wln94p6","memo":""}}
|
||||
const inpTx = cmd.tx;
|
||||
const appState = this.fnGetState();
|
||||
|
||||
// eslint-disable-next-line radix
|
||||
const sendingAmount = parseInt((parseFloat(inpTx.amount) * 10 ** 8).toFixed(0));
|
||||
const buildError = (reason: string): string => {
|
||||
const resp = {
|
||||
errorCode: -1,
|
||||
errorMessage: `Couldn't send Tx:${reason}`
|
||||
};
|
||||
|
||||
// console.log('sendtx error', resp);
|
||||
return JSON.stringify(resp);
|
||||
};
|
||||
|
||||
// First, find an address that can send the correct amount.
|
||||
const fromAddress = appState.addressesWithBalance.find(ab => ab.balance > sendingAmount);
|
||||
if (!fromAddress) {
|
||||
return buildError(`No address with sufficient balance to send ${sendingAmount}`);
|
||||
}
|
||||
|
||||
const memo = !inpTx.memo || inpTx.memo.trim() === '' ? null : inpTx.memo;
|
||||
|
||||
// Build a sendJSON object
|
||||
const sendJSON = [];
|
||||
if (memo) {
|
||||
sendJSON.push({ address: inpTx.to, amount: sendingAmount, memo });
|
||||
} else {
|
||||
sendJSON.push({ address: inpTx.to, amount: sendingAmount });
|
||||
}
|
||||
|
||||
console.log('sendjson is', sendJSON);
|
||||
|
||||
let resp;
|
||||
|
||||
try {
|
||||
const txid = this.fnSendTransaction(sendJSON);
|
||||
|
||||
// After the transaction is submitted, we return an intermediate success.
|
||||
resp = {
|
||||
version: 1.0,
|
||||
command: 'sendTx',
|
||||
result: 'success'
|
||||
};
|
||||
ws.send(this.encryptOutgoing(JSON.stringify(resp)));
|
||||
|
||||
// And then another one when the Tx was submitted successfully. For lightclient, this is the same,
|
||||
// so we end up sending 2 responses back to back
|
||||
resp = {
|
||||
version: 1.0,
|
||||
command: 'sendTxSubmitted',
|
||||
txid
|
||||
};
|
||||
} catch (err) {
|
||||
resp = {
|
||||
version: 1.0,
|
||||
command: 'sendTxFailed',
|
||||
err
|
||||
};
|
||||
}
|
||||
|
||||
return JSON.stringify(resp);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
.addressbookcontainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: 16px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.addressbooklist {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.addressbookentry {
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.addressbookentry:hover {
|
||||
background-color: #000;
|
||||
color: #c3921f;
|
||||
cursor: pointer;
|
||||
border: solid 1px grey;
|
||||
padding: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.addressbookentrybuttons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.addressbookentry .addressbookentrybuttons {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.addressbookentry:hover .addressbookentrybuttons {
|
||||
display: flex;
|
||||
}
|
|
@ -0,0 +1,200 @@
|
|||
/* eslint-disable react/prop-types */
|
||||
import React, { Component } from 'react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import {
|
||||
AccordionItemButton,
|
||||
AccordionItem,
|
||||
AccordionItemHeading,
|
||||
AccordionItemPanel,
|
||||
Accordion
|
||||
} from 'react-accessible-accordion';
|
||||
import styles from './Addressbook.css';
|
||||
import cstyles from './Common.css';
|
||||
import { AddressBookEntry } from './AppState';
|
||||
import ScrollPane from './ScrollPane';
|
||||
import Utils from '../utils/utils';
|
||||
import routes from '../constants/routes.json';
|
||||
|
||||
// Internal because we're using withRouter just below
|
||||
const AddressBookItemInteral = ({ item, removeAddressBookEntry, setSendTo, history }) => {
|
||||
return (
|
||||
<AccordionItem key={item.label} className={[cstyles.well, cstyles.margintopsmall].join(' ')} uuid={item.label}>
|
||||
<AccordionItemHeading>
|
||||
<AccordionItemButton className={cstyles.accordionHeader}>
|
||||
<div className={[cstyles.flexspacebetween].join(' ')}>
|
||||
<div>{item.label}</div>
|
||||
<div>{item.address}</div>
|
||||
</div>
|
||||
</AccordionItemButton>
|
||||
</AccordionItemHeading>
|
||||
<AccordionItemPanel>
|
||||
<div className={[cstyles.well, styles.addressbookentrybuttons].join(' ')}>
|
||||
<button
|
||||
type="button"
|
||||
className={cstyles.primarybutton}
|
||||
onClick={() => {
|
||||
setSendTo(item.address, null, null);
|
||||
history.push(routes.SEND);
|
||||
}}
|
||||
>
|
||||
Send To
|
||||
</button>
|
||||
<button type="button" className={cstyles.primarybutton} onClick={() => removeAddressBookEntry(item.label)}>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</AccordionItemPanel>
|
||||
</AccordionItem>
|
||||
);
|
||||
};
|
||||
const AddressBookItem = withRouter(AddressBookItemInteral);
|
||||
|
||||
type Props = {
|
||||
addressBook: AddressBookEntry[],
|
||||
addAddressBookEntry: (label: string, address: string) => void,
|
||||
removeAddressBookEntry: (label: string) => void,
|
||||
setSendTo: (address: string, amount: number | null, memo: string | null) => void
|
||||
};
|
||||
|
||||
type State = {
|
||||
currentLabel: string,
|
||||
currentAddress: string,
|
||||
addButtonEnabled: boolean
|
||||
};
|
||||
|
||||
export default class AddressBook extends Component<Props, State> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = { currentLabel: '', currentAddress: '', addButtonEnabled: false };
|
||||
}
|
||||
|
||||
updateLabel = (currentLabel: string) => {
|
||||
// Don't update the field if it is longer than 20 chars
|
||||
if (currentLabel.length > 20) return;
|
||||
|
||||
const { currentAddress } = this.state;
|
||||
this.setState({ currentLabel });
|
||||
|
||||
const { labelError, addressIsValid } = this.validate(currentLabel, currentAddress);
|
||||
this.setAddButtonEnabled(!labelError && addressIsValid && currentLabel !== '' && currentAddress !== '');
|
||||
};
|
||||
|
||||
updateAddress = (currentAddress: string) => {
|
||||
const { currentLabel } = this.state;
|
||||
this.setState({ currentAddress });
|
||||
|
||||
const { labelError, addressIsValid } = this.validate(currentLabel, currentAddress);
|
||||
|
||||
this.setAddButtonEnabled(!labelError && addressIsValid && currentLabel !== '' && currentAddress !== '');
|
||||
};
|
||||
|
||||
addButtonClicked = () => {
|
||||
const { addAddressBookEntry } = this.props;
|
||||
const { currentLabel, currentAddress } = this.state;
|
||||
|
||||
addAddressBookEntry(currentLabel, currentAddress);
|
||||
this.setState({ currentLabel: '', currentAddress: '' });
|
||||
};
|
||||
|
||||
setAddButtonEnabled = (addButtonEnabled: boolean) => {
|
||||
this.setState({ addButtonEnabled });
|
||||
};
|
||||
|
||||
validate = (currentLabel, currentAddress) => {
|
||||
const { addressBook } = this.props;
|
||||
|
||||
let labelError = addressBook.find(i => i.label === currentLabel) ? 'Duplicate Label' : null;
|
||||
labelError = currentLabel.length > 12 ? 'Label is too long' : labelError;
|
||||
|
||||
const addressIsValid =
|
||||
currentAddress === '' || Utils.isZaddr(currentAddress) || Utils.isTransparent(currentAddress);
|
||||
|
||||
return { labelError, addressIsValid };
|
||||
};
|
||||
|
||||
render() {
|
||||
const { addressBook, removeAddressBookEntry, setSendTo } = this.props;
|
||||
const { currentLabel, currentAddress, addButtonEnabled } = this.state;
|
||||
|
||||
const { labelError, addressIsValid } = this.validate(currentLabel, currentAddress);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={[cstyles.xlarge, cstyles.padall, cstyles.center].join(' ')}>Address Book</div>
|
||||
|
||||
<div className={styles.addressbookcontainer}>
|
||||
<div className={[cstyles.well].join(' ')}>
|
||||
<div className={[cstyles.flexspacebetween].join(' ')}>
|
||||
<div className={cstyles.sublight}>Label</div>
|
||||
<div className={cstyles.validationerror}>
|
||||
{!labelError ? (
|
||||
<i className={[cstyles.green, 'fas', 'fa-check'].join(' ')} />
|
||||
) : (
|
||||
<span className={cstyles.red}>{labelError}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={currentLabel}
|
||||
className={[cstyles.inputbox, cstyles.margintopsmall].join(' ')}
|
||||
onChange={e => this.updateLabel(e.target.value)}
|
||||
/>
|
||||
|
||||
<div className={cstyles.margintoplarge} />
|
||||
|
||||
<div className={[cstyles.flexspacebetween].join(' ')}>
|
||||
<div className={cstyles.sublight}>Address</div>
|
||||
<div className={cstyles.validationerror}>
|
||||
{addressIsValid ? (
|
||||
<i className={[cstyles.green, 'fas', 'fa-check'].join(' ')} />
|
||||
) : (
|
||||
<span className={cstyles.red}>Invalid Address</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={currentAddress}
|
||||
className={[cstyles.inputbox, cstyles.margintopsmall].join(' ')}
|
||||
onChange={e => this.updateAddress(e.target.value)}
|
||||
/>
|
||||
|
||||
<div className={cstyles.margintoplarge} />
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className={cstyles.primarybutton}
|
||||
disabled={!addButtonEnabled}
|
||||
onClick={this.addButtonClicked}
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ScrollPane offsetHeight={300}>
|
||||
<div className={styles.addressbooklist}>
|
||||
<div className={[cstyles.flexspacebetween, cstyles.tableheader, cstyles.sublight].join(' ')}>
|
||||
<div>Label</div>
|
||||
<div>Address</div>
|
||||
</div>
|
||||
{addressBook && (
|
||||
<Accordion>
|
||||
{addressBook.map(item => (
|
||||
<AddressBookItem
|
||||
key={item.label}
|
||||
item={item}
|
||||
removeAddressBookEntry={removeAddressBookEntry}
|
||||
setSendTo={setSendTo}
|
||||
/>
|
||||
))}
|
||||
</Accordion>
|
||||
)}
|
||||
</div>
|
||||
</ScrollPane>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,209 @@
|
|||
/* eslint-disable max-classes-per-file */
|
||||
|
||||
export class TotalBalance {
|
||||
// Total t address, confirmed and spendable
|
||||
transparent: number;
|
||||
|
||||
// Total private, confirmed + unconfirmed
|
||||
private: number;
|
||||
|
||||
// Total private, confirmed funds that are spendable
|
||||
verifiedPrivate: number;
|
||||
|
||||
// Total unconfirmed + spendable
|
||||
total: number;
|
||||
}
|
||||
|
||||
export class AddressBalance {
|
||||
address: string;
|
||||
|
||||
balance: number;
|
||||
|
||||
containsPending: boolean;
|
||||
|
||||
constructor(address: string, balance: number) {
|
||||
this.address = address;
|
||||
this.balance = balance;
|
||||
this.containsPending = false;
|
||||
}
|
||||
}
|
||||
|
||||
export class AddressBookEntry {
|
||||
label: string;
|
||||
|
||||
address: string;
|
||||
|
||||
constructor(label: string, address: string) {
|
||||
this.label = label;
|
||||
this.address = address;
|
||||
}
|
||||
}
|
||||
|
||||
export class TxDetail {
|
||||
address: string;
|
||||
|
||||
amount: string;
|
||||
|
||||
memo: string | null;
|
||||
}
|
||||
|
||||
// List of transactions. TODO: Handle memos, multiple addresses etc...
|
||||
export class Transaction {
|
||||
type: string;
|
||||
|
||||
address: string;
|
||||
|
||||
amount: number;
|
||||
|
||||
confirmations: number;
|
||||
|
||||
txid: string;
|
||||
|
||||
time: number;
|
||||
|
||||
detailedTxns: TxDetail[];
|
||||
}
|
||||
|
||||
export class ToAddr {
|
||||
id: number;
|
||||
|
||||
to: string;
|
||||
|
||||
amount: number;
|
||||
|
||||
memo: string;
|
||||
|
||||
constructor(id: number) {
|
||||
this.id = id;
|
||||
|
||||
this.to = '';
|
||||
this.amount = 0;
|
||||
this.memo = '';
|
||||
}
|
||||
}
|
||||
|
||||
export class SendPageState {
|
||||
fromaddr: string;
|
||||
|
||||
toaddrs: ToAddr[];
|
||||
|
||||
constructor() {
|
||||
this.fromaddr = '';
|
||||
this.toaddrs = [];
|
||||
}
|
||||
}
|
||||
|
||||
export class ReceivePageState {
|
||||
// A newly created address to show by default
|
||||
newAddress: string;
|
||||
|
||||
// The key used for the receive page component.
|
||||
// Increment to force re-render
|
||||
rerenderKey: number;
|
||||
|
||||
constructor() {
|
||||
this.newAddress = '';
|
||||
this.rerenderKey = 0;
|
||||
}
|
||||
}
|
||||
|
||||
export class RPCConfig {
|
||||
url: string;
|
||||
|
||||
constructor() {
|
||||
this.url = '';
|
||||
}
|
||||
}
|
||||
|
||||
export class Info {
|
||||
testnet: boolean;
|
||||
|
||||
latestBlock: number;
|
||||
|
||||
connections: number;
|
||||
|
||||
version: number;
|
||||
|
||||
verificationProgress: number;
|
||||
|
||||
currencyName: string;
|
||||
|
||||
solps: number;
|
||||
|
||||
zecPrice: number;
|
||||
|
||||
encrypted: boolean;
|
||||
|
||||
locked: boolean;
|
||||
}
|
||||
|
||||
export class PasswordState {
|
||||
showPassword: boolean;
|
||||
|
||||
confirmNeeded: boolean;
|
||||
|
||||
passwordCallback: (password: string) => void;
|
||||
|
||||
closeCallback: () => void;
|
||||
|
||||
helpText: string | null;
|
||||
|
||||
constructor() {
|
||||
this.showPassword = false;
|
||||
this.confirmNeeded = false;
|
||||
this.passwordCallback = null;
|
||||
this.closeCallback = null;
|
||||
this.helpText = null;
|
||||
}
|
||||
}
|
||||
|
||||
export class ConnectedCompanionApp {
|
||||
name: string;
|
||||
|
||||
lastSeen: number;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-classes-per-file
|
||||
export default class AppState {
|
||||
// The total confirmed and unconfirmed balance in this wallet
|
||||
totalBalance: TotalBalance;
|
||||
|
||||
// The list of all t and z addresses that have a current balance. That is, the list of
|
||||
// addresses that have a (confirmed or unconfirmed) UTXO or note pending.
|
||||
addressesWithBalance: AddressBalance[];
|
||||
|
||||
// A map type that contains address -> privatekey mapping, for display on the receive page
|
||||
// This mapping is ephemeral, and will disappear when the user navigates away.
|
||||
addressPrivateKeys;
|
||||
|
||||
// List of all addresses in the wallet, including change addresses and addresses
|
||||
// that don't have any balance or are unused
|
||||
addresses: string[];
|
||||
|
||||
// List of Address / Label pairs
|
||||
addressBook: AddressBookEntry[];
|
||||
|
||||
// List of all T and Z transactions
|
||||
transactions: Transaction[];
|
||||
|
||||
// The state of the send page, as the user constructs a transaction
|
||||
sendPageState: SendPageState;
|
||||
|
||||
// Any state for the receive page
|
||||
receivePageState: ReceivePageState;
|
||||
|
||||
// The Current configuration of the RPC params
|
||||
rpcConfig: RPCConfig;
|
||||
|
||||
// getinfo and getblockchaininfo result
|
||||
info: Info;
|
||||
|
||||
// Is the app rescanning?
|
||||
rescanning: boolean;
|
||||
|
||||
// Callbacks for the password dialog box
|
||||
passwordState: PasswordState;
|
||||
|
||||
// The last seen connected companion app
|
||||
connectedCompanionApp: ConnectedCompanionApp;
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
import React from 'react';
|
||||
import cstyles from './Common.css';
|
||||
import Utils from '../utils/utils';
|
||||
|
||||
// eslint-disable-next-line react/prop-types
|
||||
export const BalanceBlockHighlight = ({ zecValue, usdValue, topLabel, currencyName }) => {
|
||||
const { bigPart, smallPart } = Utils.splitZecAmountIntoBigSmall(zecValue);
|
||||
|
||||
return (
|
||||
<div style={{ padding: '1em' }}>
|
||||
{topLabel && <div className={[cstyles.small].join(' ')}>{topLabel}</div>}
|
||||
<div className={[cstyles.highlight, cstyles.xlarge].join(' ')}>
|
||||
<span>
|
||||
{currencyName} {bigPart}
|
||||
</span>
|
||||
<span className={[cstyles.small, cstyles.zecsmallpart].join(' ')}>{smallPart}</span>
|
||||
</div>
|
||||
<div className={[cstyles.sublight, cstyles.small].join(' ')}>{usdValue}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react/prop-types
|
||||
export const BalanceBlock = ({ zecValue, usdValue, topLabel, currencyName }) => {
|
||||
const { bigPart, smallPart } = Utils.splitZecAmountIntoBigSmall(zecValue);
|
||||
|
||||
return (
|
||||
<div className={cstyles.padall}>
|
||||
<div className={[cstyles.small].join(' ')}>{topLabel}</div>
|
||||
<div className={[cstyles.highlight, cstyles.large].join(' ')}>
|
||||
<span>
|
||||
{currencyName} {bigPart}
|
||||
</span>
|
||||
<span className={[cstyles.small, cstyles.zecsmallpart].join(' ')}>{smallPart}</span>
|
||||
</div>
|
||||
<div className={[cstyles.sublight, cstyles.small].join(' ')}>{usdValue}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,216 @@
|
|||
.sidebarcontainer {
|
||||
width: 220px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.contentcontainer {
|
||||
width: calc(100% - 220px);
|
||||
float: right;
|
||||
}
|
||||
|
||||
.well {
|
||||
background-color: #000;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.verticalflex {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.flexspacebetween {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
color: #c3921f;
|
||||
}
|
||||
|
||||
.xlarge {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.large {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.normal {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.small {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.fixedfont {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.sublight {
|
||||
color: #6a6a6a;
|
||||
}
|
||||
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.red {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.green {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.yellow {
|
||||
color: yellow;
|
||||
}
|
||||
|
||||
.breakword {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.primarybutton {
|
||||
background-color: #c3921f;
|
||||
color: #000;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
border-radius: 4px;
|
||||
border-color: #c3921f;
|
||||
margin-left: 8px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.primarybutton:disabled,
|
||||
.primarybutton[disabled] {
|
||||
border: 1px solid #999;
|
||||
background-color: #ccc;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.primarybutton:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.padtopsmall {
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.padbottomsmall {
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.marginbottomsmall {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.margintopsmall {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.margintoplarge {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.marginleft {
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.marginbottomlarge {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.padall {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.padsmallall {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.zecsmallpart {
|
||||
padding-left: 2px;
|
||||
}
|
||||
|
||||
.blackbg {
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
.maxwidth {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.inputbox {
|
||||
width: calc(100% - 16px);
|
||||
font-family: Roboto, Arial, Helvetica, Helvetica Neue, serif;
|
||||
min-height: 38px;
|
||||
max-height: 300px;
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
font-size: 16px;
|
||||
border: none;
|
||||
border-bottom: grey 1px solid;
|
||||
background-color: #000;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.buttoncontainer {
|
||||
text-align: center;
|
||||
padding-top: 24px;
|
||||
}
|
||||
|
||||
.modal {
|
||||
top: 25%;
|
||||
left: 12.5%;
|
||||
right: 12.5%;
|
||||
bottom: auto;
|
||||
opacity: 1;
|
||||
transform: translate(-0%, -12.5%);
|
||||
background: #212124;
|
||||
position: absolute;
|
||||
padding: 16px;
|
||||
min-width: 700px;
|
||||
}
|
||||
|
||||
.modalOverlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(100, 100, 100, 0.5);
|
||||
}
|
||||
|
||||
.accordionHeader:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.accordionHeader:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.tableheader {
|
||||
margin: 8px;
|
||||
border-bottom: solid 1px grey;
|
||||
}
|
||||
|
||||
.balancebox {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.containermargin {
|
||||
margin: 16px;
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
.addressbalancecontainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: 16px;
|
||||
margin-right: 16px;
|
||||
}
|
|
@ -0,0 +1,129 @@
|
|||
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||
/* eslint-disable no-plusplus */
|
||||
/* eslint-disable react/prop-types */
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import {
|
||||
AccordionItemButton,
|
||||
AccordionItem,
|
||||
AccordionItemHeading,
|
||||
AccordionItemPanel,
|
||||
Accordion
|
||||
} from 'react-accessible-accordion';
|
||||
import styles from './Dashboard.css';
|
||||
import cstyles from './Common.css';
|
||||
import { TotalBalance, Info, AddressBalance } from './AppState';
|
||||
import Utils from '../utils/utils';
|
||||
import ScrollPane from './ScrollPane';
|
||||
import { BalanceBlockHighlight, BalanceBlock } from './BalanceBlocks';
|
||||
|
||||
const AddressBalanceItem = ({ currencyName, zecPrice, item }) => {
|
||||
const { bigPart, smallPart } = Utils.splitZecAmountIntoBigSmall(Math.abs(item.balance));
|
||||
|
||||
return (
|
||||
<AccordionItem key={item.label} className={[cstyles.well, cstyles.margintopsmall].join(' ')} uuid={item.address}>
|
||||
<AccordionItemHeading>
|
||||
<AccordionItemButton className={cstyles.accordionHeader}>
|
||||
<div className={[cstyles.flexspacebetween].join(' ')}>
|
||||
<div>
|
||||
<div>{Utils.splitStringIntoChunks(item.address, 6).join(' ')}</div>
|
||||
{item.containsPending && (
|
||||
<div className={[cstyles.red, cstyles.small, cstyles.padtopsmall].join(' ')}>
|
||||
Some transactions are pending. Balances may change.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={[styles.txamount, cstyles.right].join(' ')}>
|
||||
<div>
|
||||
<span>
|
||||
{currencyName} {bigPart}
|
||||
</span>
|
||||
<span className={[cstyles.small, cstyles.zecsmallpart].join(' ')}>{smallPart}</span>
|
||||
</div>
|
||||
<div className={[cstyles.sublight, cstyles.small, cstyles.padtopsmall].join(' ')}>
|
||||
{Utils.getZecToUsdString(zecPrice, Math.abs(item.balance))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionItemButton>
|
||||
</AccordionItemHeading>
|
||||
<AccordionItemPanel />
|
||||
</AccordionItem>
|
||||
);
|
||||
};
|
||||
|
||||
type Props = {
|
||||
totalBalance: TotalBalance,
|
||||
info: Info,
|
||||
addressesWithBalance: AddressBalance[]
|
||||
};
|
||||
|
||||
export default class Home extends Component<Props> {
|
||||
render() {
|
||||
const { totalBalance, info, addressesWithBalance } = this.props;
|
||||
|
||||
const anyPending = addressesWithBalance && addressesWithBalance.find(i => i.containsPending);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={[cstyles.well, cstyles.containermargin].join(' ')}>
|
||||
<div className={cstyles.balancebox}>
|
||||
<BalanceBlockHighlight
|
||||
zecValue={totalBalance.total}
|
||||
usdValue={Utils.getZecToUsdString(info.zecPrice, totalBalance.total)}
|
||||
currencyName={info.currencyName}
|
||||
/>
|
||||
<BalanceBlock
|
||||
topLabel="Shielded"
|
||||
zecValue={totalBalance.private}
|
||||
usdValue={Utils.getZecToUsdString(info.zecPrice, totalBalance.private)}
|
||||
currencyName={info.currencyName}
|
||||
/>
|
||||
<BalanceBlock
|
||||
topLabel="Transparent"
|
||||
zecValue={totalBalance.transparent}
|
||||
usdValue={Utils.getZecToUsdString(info.zecPrice, totalBalance.transparent)}
|
||||
currencyName={info.currencyName}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
{anyPending && (
|
||||
<div className={[cstyles.red, cstyles.small, cstyles.padtopsmall].join(' ')}>
|
||||
Some transactions are pending. Balances may change.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.addressbalancecontainer}>
|
||||
<ScrollPane offsetHeight={200}>
|
||||
<div className={styles.addressbooklist}>
|
||||
<div className={[cstyles.flexspacebetween, cstyles.tableheader, cstyles.sublight].join(' ')}>
|
||||
<div>Address</div>
|
||||
<div>Balance</div>
|
||||
</div>
|
||||
{addressesWithBalance &&
|
||||
(addressesWithBalance.length === 0 ? (
|
||||
<div className={[cstyles.center, cstyles.sublight].join(' ')}>No Addresses with a balance</div>
|
||||
) : (
|
||||
<Accordion>
|
||||
{addressesWithBalance
|
||||
.filter(ab => ab.balance > 0)
|
||||
.map(ab => (
|
||||
<AddressBalanceItem
|
||||
key={ab.address}
|
||||
item={ab}
|
||||
currencyName={info.currencyName}
|
||||
zecPrice={info.zecPrice}
|
||||
/>
|
||||
))}
|
||||
</Accordion>
|
||||
))}
|
||||
</div>
|
||||
</ScrollPane>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
/* eslint-disable react/prop-types */
|
||||
import Modal from 'react-modal';
|
||||
import React from 'react';
|
||||
import cstyles from './Common.css';
|
||||
|
||||
export class ErrorModalData {
|
||||
title: string;
|
||||
|
||||
body: string;
|
||||
|
||||
modalIsOpen: boolean;
|
||||
|
||||
constructor() {
|
||||
this.modalIsOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
export const ErrorModal = ({ title, body, modalIsOpen, closeModal }) => {
|
||||
return (
|
||||
<Modal
|
||||
isOpen={modalIsOpen}
|
||||
onRequestClose={closeModal}
|
||||
className={cstyles.modal}
|
||||
overlayClassName={cstyles.modalOverlay}
|
||||
>
|
||||
<div className={[cstyles.verticalflex].join(' ')}>
|
||||
<div className={cstyles.marginbottomlarge} style={{ textAlign: 'center' }}>
|
||||
{title}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cstyles.well}
|
||||
style={{ textAlign: 'center', wordBreak: 'break-all', maxHeight: '400px', overflowY: 'auto' }}
|
||||
>
|
||||
{body}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={cstyles.buttoncontainer}>
|
||||
<button type="button" className={cstyles.primarybutton} onClick={closeModal}>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,10 @@
|
|||
.loadingcontainer {
|
||||
margin-left: -220px;
|
||||
}
|
||||
|
||||
.newwalletcontainer {
|
||||
flex-direction: column;
|
||||
margin-left: 10%;
|
||||
margin-right: 10%;
|
||||
text-align: left;
|
||||
}
|
|
@ -0,0 +1,368 @@
|
|||
/* eslint-disable radix */
|
||||
/* eslint-disable max-classes-per-file */
|
||||
import React, { Component } from 'react';
|
||||
import { Redirect, withRouter } from 'react-router';
|
||||
import { ipcRenderer } from 'electron';
|
||||
import TextareaAutosize from 'react-textarea-autosize';
|
||||
import native from '../../native/index.node';
|
||||
import routes from '../constants/routes.json';
|
||||
import { RPCConfig, Info } from './AppState';
|
||||
import RPC from '../rpc';
|
||||
import cstyles from './Common.css';
|
||||
import styles from './LoadingScreen.css';
|
||||
import Logo from '../assets/img/logobig.png';
|
||||
|
||||
type Props = {
|
||||
setRPCConfig: (rpcConfig: RPCConfig) => void,
|
||||
rescanning: boolean,
|
||||
setRescanning: boolean => void,
|
||||
setInfo: (info: Info) => void
|
||||
};
|
||||
|
||||
class LoadingScreenState {
|
||||
currentStatus: string;
|
||||
|
||||
loadingDone: boolean;
|
||||
|
||||
rpcConfig: RPCConfig | null;
|
||||
|
||||
url: string;
|
||||
|
||||
walletScreen: number; // 0 -> no wallet, load existing wallet 1 -> show option 2-> create new 3 -> restore existing
|
||||
|
||||
newWalletError: null | string; // Any errors when creating/restoring wallet
|
||||
|
||||
seed: string; // The new seed phrase for a newly created wallet or the seed phrase to restore from
|
||||
|
||||
birthday: number; // Wallet birthday if we're restoring
|
||||
|
||||
getinfoRetryCount: number;
|
||||
|
||||
constructor() {
|
||||
this.currentStatus = 'Loading...';
|
||||
this.loadingDone = false;
|
||||
this.rpcConfig = null;
|
||||
this.url = '';
|
||||
this.getinfoRetryCount = 0;
|
||||
this.walletScreen = 0;
|
||||
this.newWalletError = null;
|
||||
this.seed = '';
|
||||
this.birthday = 0;
|
||||
}
|
||||
}
|
||||
|
||||
class LoadingScreen extends Component<Props, LoadingScreenState> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
const state = new LoadingScreenState();
|
||||
state.url = 'https://lightwalletd.zecwallet.co:1443';
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { rescanning } = this.props;
|
||||
|
||||
if (rescanning) {
|
||||
this.runSyncStatusPoller();
|
||||
} else {
|
||||
this.doFirstTimeSetup();
|
||||
}
|
||||
}
|
||||
|
||||
doFirstTimeSetup = async () => {
|
||||
// Try to load the light client
|
||||
const { url } = this.state;
|
||||
|
||||
// First, set up the exit handler
|
||||
this.setupExitHandler();
|
||||
|
||||
// Test to see if the wallet exists
|
||||
if (!native.litelib_wallet_exists('main')) {
|
||||
// Show the wallet creation screen
|
||||
this.setState({ walletScreen: 1 });
|
||||
} else {
|
||||
const result = native.litelib_initialize_existing(true, url);
|
||||
console.log(`Intialization: ${result}`);
|
||||
if (result !== 'OK') {
|
||||
this.setState({
|
||||
currentStatus: (
|
||||
<span>
|
||||
Error Initializing Lightclient
|
||||
<br />
|
||||
{result}
|
||||
</span>
|
||||
)
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.getInfo();
|
||||
}
|
||||
};
|
||||
|
||||
setupExitHandler = () => {
|
||||
// App is quitting, make sure to save the wallet properly.
|
||||
ipcRenderer.on('appquitting', () => {
|
||||
RPC.doSave();
|
||||
|
||||
// And reply that we're all done.
|
||||
ipcRenderer.send('appquitdone');
|
||||
});
|
||||
};
|
||||
|
||||
getInfo() {
|
||||
// Try getting the info.
|
||||
try {
|
||||
// Do a sync at start
|
||||
this.setState({ currentStatus: 'Syncing...' });
|
||||
|
||||
// This will do the sync in another thread, so we have to check for sync status
|
||||
RPC.doSync();
|
||||
|
||||
this.runSyncStatusPoller();
|
||||
} catch (err) {
|
||||
// Not yet finished loading. So update the state, and setup the next refresh
|
||||
this.setState({ currentStatus: err });
|
||||
}
|
||||
}
|
||||
|
||||
runSyncStatusPoller = () => {
|
||||
const me = this;
|
||||
|
||||
const { setRPCConfig, setInfo, setRescanning } = this.props;
|
||||
const { url } = this.state;
|
||||
|
||||
const info = RPC.getInfoObject();
|
||||
|
||||
// And after a while, check the sync status.
|
||||
const poller = setInterval(() => {
|
||||
const syncstatus = RPC.doSyncStatus();
|
||||
const ss = JSON.parse(syncstatus);
|
||||
|
||||
if (ss.syncing === 'false') {
|
||||
// First, save the wallet so we don't lose the just-synced data
|
||||
RPC.doSave();
|
||||
|
||||
// Set the info object, so the sidebar will show
|
||||
console.log(info);
|
||||
setInfo(info);
|
||||
|
||||
// This will cause a redirect to the dashboard
|
||||
me.setState({ loadingDone: true });
|
||||
|
||||
setRescanning(false);
|
||||
|
||||
// Configure the RPC, which will setup the refresh
|
||||
const rpcConfig = new RPCConfig();
|
||||
rpcConfig.url = url;
|
||||
setRPCConfig(rpcConfig);
|
||||
|
||||
// And cancel the updater
|
||||
clearInterval(poller);
|
||||
} else {
|
||||
// Still syncing, grab the status and update the status
|
||||
const p = ss.synced_blocks;
|
||||
const t = ss.total_blocks;
|
||||
const currentStatus = `Syncing ${p} / ${t}`;
|
||||
|
||||
me.setState({ currentStatus });
|
||||
}
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
createNewWallet = () => {
|
||||
const { url } = this.state;
|
||||
const result = native.litelib_initialize_new(true, url);
|
||||
|
||||
if (result.startsWith('Error')) {
|
||||
this.setState({ newWalletError: result });
|
||||
} else {
|
||||
const r = JSON.parse(result);
|
||||
this.setState({ walletScreen: 2, seed: r.seed });
|
||||
}
|
||||
};
|
||||
|
||||
startNewWallet = () => {
|
||||
// Start using the new wallet
|
||||
this.setState({ walletScreen: 0 });
|
||||
this.getInfo();
|
||||
};
|
||||
|
||||
restoreExistingWallet = () => {
|
||||
this.setState({ walletScreen: 3 });
|
||||
};
|
||||
|
||||
updateSeed = e => {
|
||||
this.setState({ seed: e.target.value });
|
||||
};
|
||||
|
||||
updateBirthday = e => {
|
||||
this.setState({ birthday: e.target.value });
|
||||
};
|
||||
|
||||
restoreWalletBack = () => {
|
||||
// Reset the seed and birthday and try again
|
||||
this.setState({ seed: '', birthday: 0, newWalletError: null, walletScreen: 3 });
|
||||
};
|
||||
|
||||
doRestoreWallet = () => {
|
||||
const { seed, birthday, url } = this.state;
|
||||
console.log(`Restoring ${seed} with ${birthday}`);
|
||||
|
||||
const result = native.litelib_initialize_new_from_phrase(false, url, seed, parseInt(birthday));
|
||||
if (result.startsWith('Error')) {
|
||||
this.setState({ newWalletError: result });
|
||||
} else {
|
||||
this.setState({ walletScreen: 0 });
|
||||
this.getInfo();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { loadingDone, currentStatus, walletScreen, newWalletError, seed, birthday } = this.state;
|
||||
|
||||
// If still loading, show the status
|
||||
if (!loadingDone) {
|
||||
return (
|
||||
<div className={[cstyles.verticalflex, cstyles.center, styles.loadingcontainer].join(' ')}>
|
||||
{walletScreen === 0 && (
|
||||
<div>
|
||||
<div style={{ marginTop: '100px' }}>
|
||||
<img src={Logo} width="200px;" alt="Logo" />
|
||||
</div>
|
||||
<div>{currentStatus}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{walletScreen === 1 && (
|
||||
<div>
|
||||
<div>
|
||||
<img src={Logo} width="200px;" alt="Logo" />
|
||||
</div>
|
||||
<div className={[cstyles.well, styles.newwalletcontainer].join(' ')}>
|
||||
<div className={cstyles.verticalflex}>
|
||||
<div className={[cstyles.large, cstyles.highlight].join(' ')}>Create A New Wallet</div>
|
||||
<div className={cstyles.padtopsmall}>
|
||||
Creates a new wallet with a new randomly generated seed phrase. Please save the seed phrase
|
||||
carefully, it’s the only way to restore your wallet.
|
||||
</div>
|
||||
<div className={cstyles.margintoplarge}>
|
||||
<button type="button" className={cstyles.primarybutton} onClick={this.createNewWallet}>
|
||||
Create New
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className={[cstyles.verticalflex, cstyles.margintoplarge].join(' ')}>
|
||||
<div className={[cstyles.large, cstyles.highlight].join(' ')}>Restore Wallet From Seed</div>
|
||||
<div className={cstyles.padtopsmall}>
|
||||
If you already have a seed phrase, you can restore it to this wallet. This will rescan the
|
||||
blockchain for all transactions from the seed phrase.
|
||||
</div>
|
||||
<div className={cstyles.margintoplarge}>
|
||||
<button type="button" className={cstyles.primarybutton} onClick={this.restoreExistingWallet}>
|
||||
Restore Existing
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{walletScreen === 2 && (
|
||||
<div>
|
||||
<div>
|
||||
<img src={Logo} width="200px;" alt="Logo" />
|
||||
</div>
|
||||
<div className={[cstyles.well, styles.newwalletcontainer].join(' ')}>
|
||||
<div className={cstyles.verticalflex}>
|
||||
{newWalletError && (
|
||||
<div>
|
||||
<div className={[cstyles.large, cstyles.highlight].join(' ')}>Error Creating New Wallet</div>
|
||||
<div className={cstyles.padtopsmall}>There was an error creating a new wallet</div>
|
||||
<hr />
|
||||
<div className={cstyles.padtopsmall}>{newWalletError}</div>
|
||||
<hr />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!newWalletError && (
|
||||
<div>
|
||||
<div className={[cstyles.large, cstyles.highlight].join(' ')}>Your New Wallet</div>
|
||||
<div className={cstyles.padtopsmall}>
|
||||
This is your new wallet. Below is your seed phrase. PLEASE STORE IT CAREFULLY! The seed phrase
|
||||
is the only way to recover your funds and transactions.
|
||||
</div>
|
||||
<hr />
|
||||
<div className={cstyles.padtopsmall}>{seed}</div>
|
||||
<hr />
|
||||
<div className={cstyles.margintoplarge}>
|
||||
<button type="button" className={cstyles.primarybutton} onClick={this.startNewWallet}>
|
||||
Start Wallet
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{walletScreen === 3 && (
|
||||
<div>
|
||||
<div>
|
||||
<img src={Logo} width="200px;" alt="Logo" />
|
||||
</div>
|
||||
<div className={[cstyles.well, styles.newwalletcontainer].join(' ')}>
|
||||
<div className={cstyles.verticalflex}>
|
||||
{newWalletError && (
|
||||
<div>
|
||||
<div className={[cstyles.large, cstyles.highlight].join(' ')}>Error Restoring Wallet</div>
|
||||
<div className={cstyles.padtopsmall}>There was an error restoring your seed phrase</div>
|
||||
<hr />
|
||||
<div className={cstyles.padtopsmall}>{newWalletError}</div>
|
||||
<hr />
|
||||
<div className={cstyles.margintoplarge}>
|
||||
<button type="button" className={cstyles.primarybutton} onClick={this.restoreWalletBack}>
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!newWalletError && (
|
||||
<div>
|
||||
<div className={[cstyles.large].join(' ')}>Please enter your seed phrase</div>
|
||||
<TextareaAutosize className={cstyles.inputbox} value={seed} onChange={e => this.updateSeed(e)} />
|
||||
|
||||
<div className={[cstyles.large, cstyles.margintoplarge].join(' ')}>
|
||||
Wallet Birthday. If you don’t know this, it is OK to enter ‘0’
|
||||
</div>
|
||||
<input
|
||||
type="number"
|
||||
className={cstyles.inputbox}
|
||||
value={birthday}
|
||||
onChange={e => this.updateBirthday(e)}
|
||||
/>
|
||||
|
||||
<div className={cstyles.margintoplarge}>
|
||||
<button type="button" className={cstyles.primarybutton} onClick={this.doRestoreWallet}>
|
||||
Restore Wallet
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <Redirect to={routes.DASHBOARD} />;
|
||||
}
|
||||
}
|
||||
|
||||
export default withRouter(LoadingScreen);
|
|
@ -0,0 +1,105 @@
|
|||
// @flow
|
||||
import React, { PureComponent } from 'react';
|
||||
import Modal from 'react-modal';
|
||||
import cstyles from './Common.css';
|
||||
|
||||
type Props = {
|
||||
modalIsOpen: boolean,
|
||||
confirmNeeded: boolean,
|
||||
passwordCallback: (password: string) => void,
|
||||
closeCallback: () => void,
|
||||
helpText: string | null
|
||||
};
|
||||
|
||||
type State = {
|
||||
password: string,
|
||||
confirmPassword: string
|
||||
};
|
||||
|
||||
export default class PasswordModal extends PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = { password: '', confirmPassword: '' };
|
||||
}
|
||||
|
||||
enterButton = () => {
|
||||
const { passwordCallback } = this.props;
|
||||
const { password } = this.state;
|
||||
|
||||
passwordCallback(password);
|
||||
|
||||
// Clear the passwords
|
||||
this.setState({ password: '', confirmPassword: '' });
|
||||
};
|
||||
|
||||
closeButton = () => {
|
||||
const { closeCallback } = this.props;
|
||||
|
||||
closeCallback();
|
||||
|
||||
// Clear the passwords
|
||||
this.setState({ password: '', confirmPassword: '' });
|
||||
};
|
||||
|
||||
render() {
|
||||
const { modalIsOpen, confirmNeeded, helpText } = this.props;
|
||||
const { password, confirmPassword } = this.state;
|
||||
|
||||
const enabled = !confirmNeeded || password === confirmPassword;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={modalIsOpen}
|
||||
onRequestClose={this.closeButton}
|
||||
className={cstyles.modal}
|
||||
overlayClassName={cstyles.modalOverlay}
|
||||
>
|
||||
<div className={[cstyles.verticalflex].join(' ')}>
|
||||
<div className={cstyles.marginbottomlarge} style={{ textAlign: 'left' }}>
|
||||
{helpText && <span>{helpText}</span>}
|
||||
{!helpText && <span>Enter Wallet Password</span>}
|
||||
</div>
|
||||
|
||||
<div className={cstyles.well} style={{ textAlign: 'left' }}>
|
||||
<div className={cstyles.sublight}>Password</div>
|
||||
<input
|
||||
type="password"
|
||||
className={[cstyles.inputbox, cstyles.marginbottomlarge].join(' ')}
|
||||
value={password}
|
||||
onChange={e => this.setState({ password: e.target.value })}
|
||||
/>
|
||||
|
||||
{confirmNeeded && (
|
||||
<div>
|
||||
<div className={cstyles.sublight}>Confirm Password</div>
|
||||
<input
|
||||
type="password"
|
||||
className={[cstyles.inputbox, cstyles.marginbottomlarge].join(' ')}
|
||||
value={confirmPassword}
|
||||
onChange={e => this.setState({ confirmPassword: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={cstyles.buttoncontainer}>
|
||||
{!enabled && <div className={[cstyles.red].join(' ')}>Passwords do not match</div>}
|
||||
<button
|
||||
type="button"
|
||||
className={[cstyles.primarybutton, cstyles.margintoplarge].join(' ')}
|
||||
onClick={this.enterButton}
|
||||
disabled={!enabled}
|
||||
>
|
||||
Enter
|
||||
</button>
|
||||
|
||||
<button type="button" className={cstyles.primarybutton} onClick={this.closeButton}>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
.receivecontainer {
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
.receiveblock {
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.receiveDetail {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.receiveQrcode {
|
||||
padding: 8px;
|
||||
background-color: white;
|
||||
}
|
|
@ -0,0 +1,243 @@
|
|||
/* eslint-disable react/prop-types */
|
||||
import React, { Component, useState } from 'react';
|
||||
import { Tab, Tabs, TabList, TabPanel } from 'react-tabs';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionItem,
|
||||
AccordionItemHeading,
|
||||
AccordionItemButton,
|
||||
AccordionItemPanel
|
||||
} from 'react-accessible-accordion';
|
||||
import QRCode from 'qrcode.react';
|
||||
import { shell, clipboard } from 'electron';
|
||||
import styles from './Receive.css';
|
||||
import cstyles from './Common.css';
|
||||
import Utils from '../utils/utils';
|
||||
import { AddressBalance, Info, ReceivePageState, AddressBookEntry } from './AppState';
|
||||
import ScrollPane from './ScrollPane';
|
||||
|
||||
const AddressBlock = ({ addressBalance, label, currencyName, zecPrice, privateKey, fetchAndSetSinglePrivKey }) => {
|
||||
const { address } = addressBalance;
|
||||
|
||||
const [copied, setCopied] = useState(false);
|
||||
const balance = addressBalance.balance || 0;
|
||||
|
||||
const openAddress = () => {
|
||||
if (currencyName === 'TAZ') {
|
||||
shell.openExternal(`https://chain.so/address/ZECTEST/${address}`);
|
||||
} else {
|
||||
shell.openExternal(`https://zcha.in/accounts/${address}`);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AccordionItem key={copied} className={[cstyles.well, styles.receiveblock].join(' ')} uuid={address}>
|
||||
<AccordionItemHeading>
|
||||
<AccordionItemButton className={cstyles.accordionHeader}>{address}</AccordionItemButton>
|
||||
</AccordionItemHeading>
|
||||
<AccordionItemPanel className={[styles.receiveDetail].join(' ')}>
|
||||
<div className={[cstyles.flexspacebetween].join(' ')}>
|
||||
<div className={[cstyles.verticalflex, cstyles.marginleft].join(' ')}>
|
||||
{label && (
|
||||
<div className={cstyles.margintoplarge}>
|
||||
<div className={[cstyles.sublight].join(' ')}>Label</div>
|
||||
<div className={[cstyles.padtopsmall, cstyles.fixedfont].join(' ')}>{label}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={[cstyles.sublight, cstyles.margintoplarge].join(' ')}>Funds</div>
|
||||
<div className={[cstyles.padtopsmall].join(' ')}>
|
||||
{currencyName} {balance}
|
||||
</div>
|
||||
<div className={[cstyles.padtopsmall].join(' ')}>{Utils.getZecToUsdString(zecPrice, balance)}</div>
|
||||
|
||||
<div className={[cstyles.margintoplarge, cstyles.breakword].join(' ')}>
|
||||
{privateKey && (
|
||||
<div>
|
||||
<div className={[cstyles.sublight].join(' ')}>Private Key</div>
|
||||
<div
|
||||
className={[cstyles.breakword, cstyles.padtopsmall, cstyles.fixedfont].join(' ')}
|
||||
style={{ maxWidth: '600px' }}
|
||||
>
|
||||
{privateKey}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
className={[cstyles.primarybutton, cstyles.margintoplarge].join(' ')}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
clipboard.writeText(address);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 5000);
|
||||
}}
|
||||
>
|
||||
{copied ? <span>Copied!</span> : <span>Copy Address</span>}
|
||||
</button>
|
||||
{!privateKey && (
|
||||
<button
|
||||
className={[cstyles.primarybutton].join(' ')}
|
||||
type="button"
|
||||
onClick={() => fetchAndSetSinglePrivKey(address)}
|
||||
>
|
||||
Export Private Key
|
||||
</button>
|
||||
)}
|
||||
{Utils.isTransparent(address) && (
|
||||
<button className={[cstyles.primarybutton].join(' ')} type="button" onClick={() => openAddress()}>
|
||||
View on explorer <i className={['fas', 'fa-external-link-square-alt'].join(' ')} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<QRCode value={address} className={[styles.receiveQrcode].join(' ')} />
|
||||
</div>
|
||||
</div>
|
||||
</AccordionItemPanel>
|
||||
</AccordionItem>
|
||||
);
|
||||
};
|
||||
|
||||
type Props = {
|
||||
addresses: string[],
|
||||
addressesWithBalance: AddressBalance[],
|
||||
addressBook: AddressBookEntry[],
|
||||
info: Info,
|
||||
receivePageState: ReceivePageState,
|
||||
fetchAndSetSinglePrivKey: string => void,
|
||||
createNewAddress: boolean => void,
|
||||
rerenderKey: number
|
||||
};
|
||||
|
||||
export default class Receive extends Component<Props> {
|
||||
render() {
|
||||
const {
|
||||
addresses,
|
||||
addressesWithBalance,
|
||||
addressPrivateKeys,
|
||||
addressBook,
|
||||
info,
|
||||
receivePageState,
|
||||
fetchAndSetSinglePrivKey,
|
||||
createNewAddress,
|
||||
rerenderKey
|
||||
} = this.props;
|
||||
|
||||
// Convert the addressBalances into a map.
|
||||
const addressMap = addressesWithBalance.reduce((map, a) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
map[a.address] = a.balance;
|
||||
return map;
|
||||
}, {});
|
||||
|
||||
const zaddrs = addresses
|
||||
.filter(a => Utils.isSapling(a))
|
||||
.slice(0, 100)
|
||||
.map(a => new AddressBalance(a, addressMap[a]));
|
||||
|
||||
let defaultZaddr = zaddrs.length ? zaddrs[0].address : '';
|
||||
if (receivePageState && Utils.isSapling(receivePageState.newAddress)) {
|
||||
defaultZaddr = receivePageState.newAddress;
|
||||
|
||||
// move this address to the front, since the scrollbar will reset when we re-render
|
||||
zaddrs.sort((x, y) => {
|
||||
// eslint-disable-next-line prettier/prettier, no-nested-ternary
|
||||
return x.address === defaultZaddr ? -1 : y.address === defaultZaddr ? 1 : 0
|
||||
});
|
||||
}
|
||||
|
||||
const taddrs = addresses
|
||||
.filter(a => Utils.isTransparent(a))
|
||||
.slice(0, 100)
|
||||
.map(a => new AddressBalance(a, addressMap[a]));
|
||||
|
||||
let defaultTaddr = taddrs.length ? taddrs[0].address : '';
|
||||
if (receivePageState && Utils.isTransparent(receivePageState.newAddress)) {
|
||||
defaultTaddr = receivePageState.newAddress;
|
||||
|
||||
// move this address to the front, since the scrollbar will reset when we re-render
|
||||
taddrs.sort((x, y) => {
|
||||
// eslint-disable-next-line prettier/prettier, no-nested-ternary
|
||||
return x.address === defaultTaddr ? -1 : y.address === defaultTaddr ? 1 : 0
|
||||
});
|
||||
}
|
||||
|
||||
const addressBookMap = addressBook.reduce((map, obj) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
map[obj.address] = obj.label;
|
||||
return map;
|
||||
}, {});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.receivecontainer}>
|
||||
<Tabs>
|
||||
<TabList>
|
||||
<Tab>Shielded</Tab>
|
||||
<Tab>Transparent</Tab>
|
||||
</TabList>
|
||||
|
||||
<TabPanel key={`z${rerenderKey}`}>
|
||||
{/* Change the hardcoded height */}
|
||||
<ScrollPane offsetHeight={100}>
|
||||
<Accordion preExpanded={[defaultZaddr]}>
|
||||
{zaddrs.map(a => (
|
||||
<AddressBlock
|
||||
key={a.address}
|
||||
addressBalance={a}
|
||||
currencyName={info.currencyName}
|
||||
label={addressBookMap[a.address]}
|
||||
zecPrice={info.zecPrice}
|
||||
privateKey={addressPrivateKeys[a.address]}
|
||||
fetchAndSetSinglePrivKey={fetchAndSetSinglePrivKey}
|
||||
rerender={this.rerender}
|
||||
/>
|
||||
))}
|
||||
</Accordion>
|
||||
|
||||
<button
|
||||
className={[cstyles.primarybutton, cstyles.margintoplarge, cstyles.marginbottomlarge].join(' ')}
|
||||
onClick={() => createNewAddress(true)}
|
||||
type="button"
|
||||
>
|
||||
New Shielded Address
|
||||
</button>
|
||||
</ScrollPane>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel key={`t${rerenderKey}`}>
|
||||
{/* Change the hardcoded height */}
|
||||
<ScrollPane offsetHeight={100}>
|
||||
<Accordion preExpanded={[defaultTaddr]}>
|
||||
{taddrs.map(a => (
|
||||
<AddressBlock
|
||||
key={a.address}
|
||||
addressBalance={a}
|
||||
currencyName={info.currencyName}
|
||||
zecPrice={info.zecPrice}
|
||||
privateKey={addressPrivateKeys[a.address]}
|
||||
fetchAndSetSinglePrivKey={fetchAndSetSinglePrivKey}
|
||||
rerender={this.rerender}
|
||||
/>
|
||||
))}
|
||||
</Accordion>
|
||||
|
||||
<button
|
||||
className={[cstyles.primarybutton, cstyles.margintoplarge, cstyles.marginbottomlarge].join(' ')}
|
||||
type="button"
|
||||
onClick={() => createNewAddress(false)}
|
||||
>
|
||||
New Transparent Address
|
||||
</button>
|
||||
</ScrollPane>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
type PaneState = {
|
||||
height: number
|
||||
};
|
||||
|
||||
type Props = {
|
||||
children: PropTypes.node.isRequired,
|
||||
className: PropTypes.node.isRequired,
|
||||
offsetHeight: number
|
||||
};
|
||||
|
||||
export default class ScrollPane extends Component<Props, PaneState> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = { height: 0 };
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.updateDimensions();
|
||||
window.addEventListener('resize', this.updateDimensions.bind(this));
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('resize', this.updateDimensions.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate & Update state of height, needed for the scrolling
|
||||
*/
|
||||
updateDimensions() {
|
||||
// eslint-disable-next-line react/destructuring-assignment
|
||||
const updateHeight = window.innerHeight - this.props.offsetHeight;
|
||||
this.setState({ height: updateHeight });
|
||||
}
|
||||
|
||||
render() {
|
||||
const { children, className } = this.props;
|
||||
const { height } = this.state;
|
||||
|
||||
return (
|
||||
<div className={className} style={{ overflowY: 'auto', overflowX: 'hidden', height }}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
.toaddrcontainer {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.toaddrbutton {
|
||||
height: 24px;
|
||||
margin-top: 13px;
|
||||
}
|
||||
|
||||
.toaddrbutton:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.confirmModal {
|
||||
top: 25%;
|
||||
left: 25%;
|
||||
right: auto;
|
||||
bottom: auto;
|
||||
opacity: 1;
|
||||
transform: translate(-12.5%, -12.5%);
|
||||
background: #212124;
|
||||
position: absolute;
|
||||
padding: 16px;
|
||||
min-width: 700px;
|
||||
}
|
||||
|
||||
.confirmModalAddress {
|
||||
width: 60%;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.confirmOverlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(100, 100, 100, 0.5);
|
||||
}
|
|
@ -0,0 +1,585 @@
|
|||
/* eslint-disable no-restricted-globals */
|
||||
/* eslint-disable no-else-return */
|
||||
/* eslint-disable radix */
|
||||
/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */
|
||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||
/* eslint-disable react/prop-types */
|
||||
/* eslint-disable max-classes-per-file */
|
||||
// @flow
|
||||
import React, { PureComponent } from 'react';
|
||||
import Modal from 'react-modal';
|
||||
import TextareaAutosize from 'react-textarea-autosize';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import styles from './Send.css';
|
||||
import cstyles from './Common.css';
|
||||
import { ToAddr, AddressBalance, SendPageState, Info, AddressBookEntry, TotalBalance } from './AppState';
|
||||
import Utils from '../utils/utils';
|
||||
import ScrollPane from './ScrollPane';
|
||||
import ArrowUpLight from '../assets/img/arrow_up_dark.png';
|
||||
import { ErrorModal } from './ErrorModal';
|
||||
import { BalanceBlockHighlight } from './BalanceBlocks';
|
||||
import routes from '../constants/routes.json';
|
||||
|
||||
type OptionType = {
|
||||
value: string,
|
||||
label: string
|
||||
};
|
||||
|
||||
const Spacer = () => {
|
||||
return <div style={{ marginTop: '24px' }} />;
|
||||
};
|
||||
|
||||
// $FlowFixMe
|
||||
const ToAddrBox = ({
|
||||
toaddr,
|
||||
zecPrice,
|
||||
updateToField,
|
||||
fromAddress,
|
||||
fromAmount,
|
||||
setMaxAmount,
|
||||
setSendButtonEnable,
|
||||
totalAmountAvailable
|
||||
}) => {
|
||||
const isMemoDisabled = !Utils.isZaddr(toaddr.to);
|
||||
|
||||
const addressIsValid = toaddr.to === '' || Utils.isZaddr(toaddr.to) || Utils.isTransparent(toaddr.to);
|
||||
const memoIsValid = toaddr.memo.length <= 512;
|
||||
|
||||
let amountError = null;
|
||||
if (toaddr.amount) {
|
||||
if (toaddr.amount < 0) {
|
||||
amountError = 'Amount cannot be negative';
|
||||
}
|
||||
if (toaddr.amount > fromAmount) {
|
||||
amountError = 'Amount Exceeds Balance';
|
||||
}
|
||||
const s = toaddr.amount.toString().split('.');
|
||||
if (s && s.length > 1 && s[1].length > 8) {
|
||||
amountError = 'Too Many Decimals';
|
||||
}
|
||||
}
|
||||
|
||||
if (isNaN(toaddr.amount)) {
|
||||
// Amount is empty
|
||||
amountError = 'Amount cannot be empty';
|
||||
}
|
||||
|
||||
if (
|
||||
!addressIsValid ||
|
||||
amountError ||
|
||||
!memoIsValid ||
|
||||
toaddr.to === '' ||
|
||||
parseFloat(toaddr.amount) === 0 ||
|
||||
fromAmount === 0
|
||||
) {
|
||||
setSendButtonEnable(false);
|
||||
} else {
|
||||
setSendButtonEnable(true);
|
||||
}
|
||||
|
||||
const usdValue = Utils.getZecToUsdString(zecPrice, toaddr.amount);
|
||||
|
||||
const addReplyTo = () => {
|
||||
if (toaddr.memo.endsWith(fromAddress)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (fromAddress) {
|
||||
updateToField(toaddr.id, null, null, `${toaddr.memo}\nReply-To:\n${fromAddress}`);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={[cstyles.well, cstyles.verticalflex].join(' ')}>
|
||||
<div className={[cstyles.flexspacebetween].join(' ')}>
|
||||
<div className={cstyles.sublight}>To</div>
|
||||
<div className={cstyles.validationerror}>
|
||||
{addressIsValid ? (
|
||||
<i className={[cstyles.green, 'fas', 'fa-check'].join(' ')} />
|
||||
) : (
|
||||
<span className={cstyles.red}>Invalid Address</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Z or T address"
|
||||
className={cstyles.inputbox}
|
||||
value={toaddr.to}
|
||||
onChange={e => updateToField(toaddr.id, e, null, null)}
|
||||
/>
|
||||
<Spacer />
|
||||
<div className={[cstyles.flexspacebetween].join(' ')}>
|
||||
<div className={cstyles.sublight}>Amount</div>
|
||||
<div className={cstyles.validationerror}>
|
||||
{amountError ? <span className={cstyles.red}>{amountError}</span> : <span>{usdValue}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className={[cstyles.flexspacebetween].join(' ')}>
|
||||
<input
|
||||
type="number"
|
||||
className={cstyles.inputbox}
|
||||
value={toaddr.amount}
|
||||
onChange={e => updateToField(toaddr.id, null, e, null)}
|
||||
/>
|
||||
<img
|
||||
className={styles.toaddrbutton}
|
||||
src={ArrowUpLight}
|
||||
alt="Max"
|
||||
onClick={() => setMaxAmount(toaddr.id, totalAmountAvailable)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Spacer />
|
||||
|
||||
{isMemoDisabled && <div className={cstyles.sublight}>Memos only for z-addresses</div>}
|
||||
|
||||
{!isMemoDisabled && (
|
||||
<div>
|
||||
<div className={[cstyles.flexspacebetween].join(' ')}>
|
||||
<div className={cstyles.sublight}>Memo</div>
|
||||
<div className={cstyles.validationerror}>
|
||||
{memoIsValid ? toaddr.memo.length : <span className={cstyles.red}>{toaddr.memo.length}</span>} / 512
|
||||
</div>
|
||||
</div>
|
||||
<TextareaAutosize
|
||||
className={cstyles.inputbox}
|
||||
value={toaddr.memo}
|
||||
disabled={isMemoDisabled}
|
||||
onChange={e => updateToField(toaddr.id, null, null, e)}
|
||||
/>
|
||||
<input type="checkbox" onChange={e => e.target.checked && addReplyTo()} />
|
||||
Include Reply-To address
|
||||
</div>
|
||||
)}
|
||||
<Spacer />
|
||||
</div>
|
||||
<Spacer />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function getSendManyJSON(sendPageState: SendPageState): [] {
|
||||
const json = sendPageState.toaddrs.map(to => {
|
||||
const memo = to.memo || '';
|
||||
const amount = parseInt((parseFloat(to.amount) * 10 ** 8).toFixed(0));
|
||||
|
||||
if (memo === '') {
|
||||
return { address: to.to, amount };
|
||||
} else {
|
||||
return { address: to.to, amount, memo };
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Sending:');
|
||||
console.log(json);
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
const ConfirmModalToAddr = ({ toaddr, info }) => {
|
||||
const { bigPart, smallPart } = Utils.splitZecAmountIntoBigSmall(toaddr.amount);
|
||||
|
||||
const memo: string = toaddr.memo ? toaddr.memo : '';
|
||||
|
||||
return (
|
||||
<div className={cstyles.well}>
|
||||
<div className={[cstyles.flexspacebetween, cstyles.margintoplarge].join(' ')}>
|
||||
<div className={[styles.confirmModalAddress].join(' ')}>
|
||||
{Utils.splitStringIntoChunks(toaddr.to, 6).join(' ')}
|
||||
</div>
|
||||
<div className={[cstyles.verticalflex, cstyles.right].join(' ')}>
|
||||
<div className={cstyles.large}>
|
||||
<div>
|
||||
<span>
|
||||
{info.currencyName} {bigPart}
|
||||
</span>
|
||||
<span className={[cstyles.small, styles.zecsmallpart].join(' ')}>{smallPart}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>{Utils.getZecToUsdString(info.zecPrice, toaddr.amount)}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={[cstyles.sublight, cstyles.breakword].join(' ')}>{memo}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Internal because we're using withRouter just below
|
||||
const ConfirmModalInternal = ({
|
||||
sendPageState,
|
||||
info,
|
||||
sendTransaction,
|
||||
clearToAddrs,
|
||||
closeModal,
|
||||
modalIsOpen,
|
||||
openErrorModal,
|
||||
openPasswordAndUnlockIfNeeded,
|
||||
history
|
||||
}) => {
|
||||
const sendingTotal = sendPageState.toaddrs.reduce((s, t) => parseFloat(s) + parseFloat(t.amount), 0.0) + 0.0001;
|
||||
const { bigPart, smallPart } = Utils.splitZecAmountIntoBigSmall(sendingTotal);
|
||||
|
||||
const sendButton = () => {
|
||||
// First, close the confirm modal.
|
||||
closeModal();
|
||||
|
||||
// This will be replaced by either a success TXID or error message that the user
|
||||
// has to close manually.
|
||||
openErrorModal('Computing Transaction', 'Please wait...This could take a while');
|
||||
|
||||
// Now, send the Tx in a timeout, so that the error modal above has a chance to display
|
||||
setTimeout(() => {
|
||||
openPasswordAndUnlockIfNeeded(() => {
|
||||
// Then send the Tx async
|
||||
(async () => {
|
||||
const sendJson = getSendManyJSON(sendPageState);
|
||||
let txid = '';
|
||||
|
||||
try {
|
||||
txid = sendTransaction(sendJson);
|
||||
|
||||
openErrorModal(
|
||||
'Successfully Broadcast Transaction',
|
||||
`Transaction was successfully broadcast.\nTXID: ${txid}`
|
||||
);
|
||||
|
||||
clearToAddrs();
|
||||
|
||||
// Redirect to dashboard after
|
||||
history.push(routes.DASHBOARD);
|
||||
} catch (err) {
|
||||
// If there was an error, show the error modal
|
||||
openErrorModal('Error Sending Transaction', err);
|
||||
}
|
||||
})();
|
||||
});
|
||||
}, 10);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={modalIsOpen}
|
||||
onRequestClose={closeModal}
|
||||
className={styles.confirmModal}
|
||||
overlayClassName={styles.confirmOverlay}
|
||||
>
|
||||
<div className={[cstyles.verticalflex].join(' ')}>
|
||||
<div className={[cstyles.marginbottomlarge, cstyles.center].join(' ')}>Confirm Transaction</div>
|
||||
<div className={cstyles.flex}>
|
||||
<div
|
||||
className={[
|
||||
cstyles.highlight,
|
||||
cstyles.xlarge,
|
||||
cstyles.flexspacebetween,
|
||||
cstyles.well,
|
||||
cstyles.maxwidth
|
||||
].join(' ')}
|
||||
>
|
||||
<div>Total</div>
|
||||
<div className={[cstyles.right, cstyles.verticalflex].join(' ')}>
|
||||
<div>
|
||||
<span>
|
||||
{info.currencyName} {bigPart}
|
||||
</span>
|
||||
<span className={[cstyles.small, styles.zecsmallpart].join(' ')}>{smallPart}</span>
|
||||
</div>
|
||||
|
||||
<div className={cstyles.normal}>{Utils.getZecToUsdString(info.zecPrice, sendingTotal)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={[cstyles.verticalflex, cstyles.margintoplarge].join(' ')}>
|
||||
{sendPageState.toaddrs.map(t => (
|
||||
<ConfirmModalToAddr key={t.to} toaddr={t} info={info} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<ConfirmModalToAddr toaddr={{ to: 'Fee', amount: 0.0001, memo: null }} info={info} />
|
||||
|
||||
<div className={cstyles.buttoncontainer}>
|
||||
<button type="button" className={cstyles.primarybutton} onClick={() => sendButton()}>
|
||||
Send
|
||||
</button>
|
||||
<button type="button" className={cstyles.primarybutton} onClick={closeModal}>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const ConfirmModal = withRouter(ConfirmModalInternal);
|
||||
|
||||
type Props = {
|
||||
addresses: string[],
|
||||
totalBalance: TotalBalance,
|
||||
addressBook: AddressBookEntry[],
|
||||
sendPageState: SendPageState,
|
||||
sendTransaction: (sendJson: []) => string,
|
||||
setSendPageState: (sendPageState: SendPageState) => void,
|
||||
openErrorModal: (title: string, body: string) => void,
|
||||
closeErrorModal: () => void,
|
||||
info: Info,
|
||||
openPasswordAndUnlockIfNeeded: (successCallback: () => void) => void
|
||||
};
|
||||
|
||||
class SendState {
|
||||
modalIsOpen: boolean;
|
||||
|
||||
errorModalIsOpen: boolean;
|
||||
|
||||
errorModalTitle: string;
|
||||
|
||||
errorModalBody: string;
|
||||
|
||||
sendButtonEnabled: boolean;
|
||||
|
||||
constructor() {
|
||||
this.modalIsOpen = false;
|
||||
this.errorModalIsOpen = false;
|
||||
this.errorModalBody = '';
|
||||
this.errorModalTitle = '';
|
||||
this.sendButtonEnabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
export default class Send extends PureComponent<Props, SendState> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = new SendState();
|
||||
}
|
||||
|
||||
addToAddr = () => {
|
||||
const { sendPageState, setSendPageState } = this.props;
|
||||
const newToAddrs = sendPageState.toaddrs.concat(new ToAddr(Utils.getNextToAddrID()));
|
||||
|
||||
// Create the new state object
|
||||
const newState = new SendPageState();
|
||||
newState.fromaddr = sendPageState.fromaddr;
|
||||
newState.toaddrs = newToAddrs;
|
||||
|
||||
setSendPageState(newState);
|
||||
};
|
||||
|
||||
clearToAddrs = () => {
|
||||
const { sendPageState, setSendPageState } = this.props;
|
||||
const newToAddrs = [new ToAddr(Utils.getNextToAddrID())];
|
||||
|
||||
// Create the new state object
|
||||
const newState = new SendPageState();
|
||||
newState.fromaddr = sendPageState.fromaddr;
|
||||
newState.toaddrs = newToAddrs;
|
||||
|
||||
setSendPageState(newState);
|
||||
};
|
||||
|
||||
changeFrom = (selectedOption: OptionType) => {
|
||||
const { sendPageState, setSendPageState } = this.props;
|
||||
|
||||
// Create the new state object
|
||||
const newState = new SendPageState();
|
||||
newState.fromaddr = selectedOption.value;
|
||||
newState.toaddrs = sendPageState.toaddrs;
|
||||
|
||||
setSendPageState(newState);
|
||||
};
|
||||
|
||||
updateToField = (id: number, address: Event | null, amount: Event | null, memo: Event | string | null) => {
|
||||
const { sendPageState, setSendPageState } = this.props;
|
||||
|
||||
const newToAddrs = sendPageState.toaddrs.slice(0);
|
||||
// Find the correct toAddr
|
||||
const toAddr = newToAddrs.find(a => a.id === id);
|
||||
if (address) {
|
||||
// $FlowFixMe
|
||||
toAddr.to = address.target.value.replace(/ /g, ''); // Remove spaces
|
||||
}
|
||||
|
||||
if (amount) {
|
||||
// Check to see the new amount if valid
|
||||
// $FlowFixMe
|
||||
const newAmount = parseFloat(amount.target.value);
|
||||
if (newAmount < 0 || newAmount > 21 * 10 ** 6) {
|
||||
return;
|
||||
}
|
||||
// $FlowFixMe
|
||||
toAddr.amount = newAmount;
|
||||
}
|
||||
|
||||
if (memo) {
|
||||
if (typeof memo === 'string') {
|
||||
toAddr.memo = memo;
|
||||
} else {
|
||||
// $FlowFixMe
|
||||
toAddr.memo = memo.target.value;
|
||||
}
|
||||
}
|
||||
|
||||
// Create the new state object
|
||||
const newState = new SendPageState();
|
||||
newState.fromaddr = sendPageState.fromaddr;
|
||||
newState.toaddrs = newToAddrs;
|
||||
|
||||
setSendPageState(newState);
|
||||
};
|
||||
|
||||
setMaxAmount = (id: number, total: number) => {
|
||||
const { sendPageState, setSendPageState } = this.props;
|
||||
|
||||
const newToAddrs = sendPageState.toaddrs.slice(0);
|
||||
|
||||
let totalOtherAmount: number = newToAddrs
|
||||
.filter(a => a.id !== id)
|
||||
.reduce((s, a) => parseFloat(s) + parseFloat(a.amount), 0);
|
||||
|
||||
// Add Fee
|
||||
totalOtherAmount += 0.0001;
|
||||
|
||||
// Find the correct toAddr
|
||||
const toAddr = newToAddrs.find(a => a.id === id);
|
||||
toAddr.amount = total - totalOtherAmount;
|
||||
if (toAddr.amount < 0) toAddr.amount = 0;
|
||||
toAddr.amount = Utils.maxPrecision(toAddr.amount);
|
||||
|
||||
// Create the new state object
|
||||
const newState = new SendPageState();
|
||||
newState.fromaddr = sendPageState.fromaddr;
|
||||
newState.toaddrs = newToAddrs;
|
||||
|
||||
setSendPageState(newState);
|
||||
};
|
||||
|
||||
setSendButtonEnable = (sendButtonEnabled: boolean) => {
|
||||
this.setState({ sendButtonEnabled });
|
||||
};
|
||||
|
||||
openModal = () => {
|
||||
this.setState({ modalIsOpen: true });
|
||||
};
|
||||
|
||||
closeModal = () => {
|
||||
this.setState({ modalIsOpen: false });
|
||||
};
|
||||
|
||||
getBalanceForAddress = (addr: string, addressesWithBalance: AddressBalance[]): number => {
|
||||
// Find the addr in addressesWithBalance
|
||||
const addressBalance: AddressBalance = addressesWithBalance.find(ab => ab.address === addr);
|
||||
|
||||
if (!addressBalance) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return addressBalance.balance;
|
||||
};
|
||||
|
||||
getLabelForFromAddress = (addr: string, addressesWithBalance: AddressBalance[], currencyName: string) => {
|
||||
// Find the addr in addressesWithBalance
|
||||
const { addressBook } = this.props;
|
||||
const label = addressBook.find(ab => ab.address === addr);
|
||||
const labelStr = label ? ` [ ${label.label} ]` : '';
|
||||
|
||||
const balance = this.getBalanceForAddress(addr, addressesWithBalance);
|
||||
|
||||
return `[ ${currencyName} ${balance.toString()} ]${labelStr} ${addr}`;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { modalIsOpen, errorModalIsOpen, errorModalTitle, errorModalBody, sendButtonEnabled } = this.state;
|
||||
const {
|
||||
addresses,
|
||||
sendTransaction,
|
||||
sendPageState,
|
||||
info,
|
||||
totalBalance,
|
||||
openErrorModal,
|
||||
closeErrorModal,
|
||||
openPasswordAndUnlockIfNeeded
|
||||
} = this.props;
|
||||
|
||||
const totalAmountAvailable = totalBalance.transparent + totalBalance.verifiedPrivate;
|
||||
const fromaddr = addresses.find(a => Utils.isSapling(a));
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={[cstyles.xlarge, cstyles.padall, cstyles.center].join(' ')}>Send</div>
|
||||
|
||||
<div className={styles.sendcontainer}>
|
||||
<div className={[cstyles.well, cstyles.balancebox, cstyles.containermargin].join(' ')}>
|
||||
<BalanceBlockHighlight
|
||||
topLabel="Confirmed Funds"
|
||||
zecValue={totalAmountAvailable}
|
||||
usdValue={Utils.getZecToUsdString(info.zecPrice, totalAmountAvailable)}
|
||||
currencyName={info.currencyName}
|
||||
/>
|
||||
<BalanceBlockHighlight
|
||||
topLabel="All Funds"
|
||||
zecValue={totalBalance.total}
|
||||
usdValue={Utils.getZecToUsdString(info.zecPrice, totalBalance.total)}
|
||||
currencyName={info.currencyName}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ScrollPane className={cstyles.containermargin} offsetHeight={320}>
|
||||
{sendPageState.toaddrs.map(toaddr => {
|
||||
return (
|
||||
<ToAddrBox
|
||||
key={toaddr.id}
|
||||
toaddr={toaddr}
|
||||
zecPrice={info.zecPrice}
|
||||
updateToField={this.updateToField}
|
||||
fromAddress={fromaddr}
|
||||
fromAmount={totalAmountAvailable}
|
||||
setMaxAmount={this.setMaxAmount}
|
||||
setSendButtonEnable={this.setSendButtonEnable}
|
||||
totalAmountAvailable={totalAmountAvailable}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<div style={{ textAlign: 'right' }}>
|
||||
<button type="button" onClick={this.addToAddr}>
|
||||
<i className={['fas', 'fa-plus'].join(' ')} />
|
||||
</button>
|
||||
</div>
|
||||
</ScrollPane>
|
||||
|
||||
<div className={cstyles.center}>
|
||||
<button
|
||||
type="button"
|
||||
disabled={!sendButtonEnabled}
|
||||
className={cstyles.primarybutton}
|
||||
onClick={this.openModal}
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
<button type="button" className={cstyles.primarybutton} onClick={this.clearToAddrs}>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ConfirmModal
|
||||
sendPageState={sendPageState}
|
||||
info={info}
|
||||
sendTransaction={sendTransaction}
|
||||
openErrorModal={openErrorModal}
|
||||
closeModal={this.closeModal}
|
||||
modalIsOpen={modalIsOpen}
|
||||
clearToAddrs={this.clearToAddrs}
|
||||
openPasswordAndUnlockIfNeeded={openPasswordAndUnlockIfNeeded}
|
||||
/>
|
||||
|
||||
<ErrorModal
|
||||
title={errorModalTitle}
|
||||
body={errorModalBody}
|
||||
modalIsOpen={errorModalIsOpen}
|
||||
closeModal={closeErrorModal}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
.sidebar {
|
||||
background-color: #000;
|
||||
height: calc(100vh - 135px);
|
||||
}
|
||||
|
||||
.sidebarmenuitem {
|
||||
height: 40px;
|
||||
padding: 10px;
|
||||
margin-left: 16px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.sidebarmenuitemactive {
|
||||
color: #c3921f;
|
||||
}
|
||||
|
||||
.sidebarlogobg {
|
||||
background: #ffb100;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.exportedPrivKeys {
|
||||
width: 100%;
|
||||
max-height: calc(100vh - 400px);
|
||||
}
|
|
@ -0,0 +1,513 @@
|
|||
/* eslint-disable react/destructuring-assignment */
|
||||
/* eslint-disable react/prop-types */
|
||||
import React, { PureComponent } from 'react';
|
||||
import type { Element } from 'react';
|
||||
import url from 'url';
|
||||
import querystring from 'querystring';
|
||||
import Modal from 'react-modal';
|
||||
import { withRouter } from 'react-router';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { ipcRenderer } from 'electron';
|
||||
import TextareaAutosize from 'react-textarea-autosize';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './Sidebar.css';
|
||||
import cstyles from './Common.css';
|
||||
import routes from '../constants/routes.json';
|
||||
import Logo from '../assets/img/logobig.png';
|
||||
import { Info } from './AppState';
|
||||
import Utils from '../utils/utils';
|
||||
import RPC from '../rpc';
|
||||
|
||||
const ExportPrivKeyModal = ({ modalIsOpen, exportedPrivKeys, closeModal }) => {
|
||||
return (
|
||||
<Modal
|
||||
isOpen={modalIsOpen}
|
||||
onRequestClose={closeModal}
|
||||
className={cstyles.modal}
|
||||
overlayClassName={cstyles.modalOverlay}
|
||||
>
|
||||
<div className={[cstyles.verticalflex].join(' ')}>
|
||||
<div className={cstyles.marginbottomlarge} style={{ textAlign: 'center' }}>
|
||||
Your Wallet Private Keys
|
||||
</div>
|
||||
|
||||
<div className={[cstyles.marginbottomlarge, cstyles.center].join(' ')}>
|
||||
These are all the private keys in your wallet. Please store them carefully!
|
||||
</div>
|
||||
|
||||
{exportedPrivKeys && (
|
||||
<TextareaAutosize value={exportedPrivKeys.join('\n')} className={styles.exportedPrivKeys} disabled />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={cstyles.buttoncontainer}>
|
||||
<button type="button" className={cstyles.primarybutton} onClick={closeModal}>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const PayURIModal = ({
|
||||
modalIsOpen,
|
||||
modalInput,
|
||||
setModalInput,
|
||||
closeModal,
|
||||
modalTitle,
|
||||
actionButtonName,
|
||||
actionCallback
|
||||
}) => {
|
||||
return (
|
||||
<Modal
|
||||
isOpen={modalIsOpen}
|
||||
onRequestClose={closeModal}
|
||||
className={cstyles.modal}
|
||||
overlayClassName={cstyles.modalOverlay}
|
||||
>
|
||||
<div className={[cstyles.verticalflex].join(' ')}>
|
||||
<div className={cstyles.marginbottomlarge} style={{ textAlign: 'center' }}>
|
||||
{modalTitle}
|
||||
</div>
|
||||
|
||||
<div className={cstyles.well} style={{ textAlign: 'center' }}>
|
||||
<input
|
||||
type="text"
|
||||
className={cstyles.inputbox}
|
||||
placeholder="URI"
|
||||
value={modalInput}
|
||||
onChange={e => setModalInput(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={cstyles.buttoncontainer}>
|
||||
{actionButtonName && (
|
||||
<button
|
||||
type="button"
|
||||
className={cstyles.primarybutton}
|
||||
onClick={() => {
|
||||
if (modalInput) {
|
||||
actionCallback(modalInput);
|
||||
}
|
||||
closeModal();
|
||||
}}
|
||||
>
|
||||
{actionButtonName}
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button type="button" className={cstyles.primarybutton} onClick={closeModal}>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const SidebarMenuItem = ({ name, routeName, currentRoute, iconname }) => {
|
||||
let isActive = false;
|
||||
|
||||
if ((currentRoute.endsWith('app.html') && routeName === routes.HOME) || currentRoute === routeName) {
|
||||
isActive = true;
|
||||
}
|
||||
|
||||
let activeColorClass = '';
|
||||
if (isActive) {
|
||||
activeColorClass = styles.sidebarmenuitemactive;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={[styles.sidebarmenuitem, activeColorClass].join(' ')}>
|
||||
<Link to={routeName}>
|
||||
<span className={activeColorClass}>
|
||||
<i className={['fas', iconname].join(' ')} />
|
||||
|
||||
{name}
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type Props = {
|
||||
info: Info,
|
||||
setRescanning: boolean => void,
|
||||
addresses: string[],
|
||||
setInfo: Info => void,
|
||||
setSendTo: (address: string, amount: number | null, memo: string | null) => void,
|
||||
getPrivKeyAsString: (address: string) => string,
|
||||
history: PropTypes.object.isRequired,
|
||||
openErrorModal: (title: string, body: string | Element<'div'>) => void,
|
||||
openPassword: (boolean, (string) => void, () => void, string) => void,
|
||||
openPasswordAndUnlockIfNeeded: (successCallback: () => void) => void,
|
||||
lockWallet: () => void,
|
||||
encryptWallet: string => void,
|
||||
decryptWallet: string => void
|
||||
};
|
||||
|
||||
type State = {
|
||||
uriModalIsOpen: boolean,
|
||||
uriModalInputValue: string | null,
|
||||
exportPrivKeysModalIsOpen: boolean,
|
||||
exportedPrivKeys: string[] | null
|
||||
};
|
||||
|
||||
class Sidebar extends PureComponent<Props, State> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
uriModalIsOpen: false,
|
||||
uriModalInputValue: null,
|
||||
exportPrivKeysModalIsOpen: false,
|
||||
exportedPrivKeys: null
|
||||
};
|
||||
|
||||
this.setupMenuHandlers();
|
||||
}
|
||||
|
||||
// Handle menu items
|
||||
setupMenuHandlers = async () => {
|
||||
const { setSendTo, setInfo, setRescanning, history, openErrorModal, openPasswordAndUnlockIfNeeded } = this.props;
|
||||
|
||||
// About
|
||||
ipcRenderer.on('about', () => {
|
||||
openErrorModal(
|
||||
'Zecwallet Lite',
|
||||
<div className={cstyles.verticalflex}>
|
||||
<div className={cstyles.margintoplarge}>Zecwallet Lite v1.1.0-beta1</div>
|
||||
<div className={cstyles.margintoplarge}>Built with Electron. Copyright (c) 2018-2020, Aditya Kulkarni.</div>
|
||||
<div className={cstyles.margintoplarge}>
|
||||
The MIT License (MIT) Copyright (c) 2018-2020 Zecwallet
|
||||
<br />
|
||||
<br />
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
|
||||
following conditions:
|
||||
<br />
|
||||
<br />
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||
portions of the Software.
|
||||
<br />
|
||||
<br />
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
|
||||
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
||||
NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
||||
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
// Donate button
|
||||
ipcRenderer.on('donate', () => {
|
||||
const { info } = this.props;
|
||||
|
||||
setSendTo(
|
||||
Utils.getDonationAddress(info.testnet),
|
||||
Utils.getDefaultDonationAmount(info.testnet),
|
||||
Utils.getDefaultDonationMemo(info.testnet)
|
||||
);
|
||||
|
||||
history.push(routes.SEND);
|
||||
});
|
||||
|
||||
// Pay URI
|
||||
ipcRenderer.on('payuri', (event, uri) => {
|
||||
this.openURIModal(uri);
|
||||
});
|
||||
|
||||
// Export Seed
|
||||
ipcRenderer.on('seed', () => {
|
||||
openPasswordAndUnlockIfNeeded(() => {
|
||||
const seed = RPC.fetchSeed();
|
||||
|
||||
openErrorModal(
|
||||
'Wallet Seed',
|
||||
<div className={cstyles.verticalflex}>
|
||||
<div>
|
||||
This is your wallet’s seed phrase. It can be used to recover your entire wallet.
|
||||
<br />
|
||||
PLEASE KEEP IT SAFE!
|
||||
</div>
|
||||
<hr />
|
||||
<div style={{ wordBreak: 'break-word', fontFamily: 'monospace, Roboto' }}>{seed}</div>
|
||||
<hr />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// Encrypt wallet
|
||||
ipcRenderer.on('encrypt', async () => {
|
||||
const { info, lockWallet, encryptWallet, openPassword } = this.props;
|
||||
|
||||
if (info.encrypted && info.locked) {
|
||||
openErrorModal('Already Encrypted', 'Your wallet is already encrypted and locked.');
|
||||
} else if (info.encrypted && !info.locked) {
|
||||
await lockWallet();
|
||||
openErrorModal('Locked', 'Your wallet has been locked. A password will be needed to spend funds.');
|
||||
} else {
|
||||
// Encrypt the wallet
|
||||
openPassword(
|
||||
true,
|
||||
async password => {
|
||||
await encryptWallet(password);
|
||||
openErrorModal('Encrypted', 'Your wallet has been encrypted. The password will be needed to spend funds.');
|
||||
},
|
||||
() => {
|
||||
openErrorModal('Cancelled', 'Your wallet was not encrypted.');
|
||||
},
|
||||
<div>
|
||||
Please enter a password to encrypt your wallet. <br />
|
||||
WARNING: If you forget this password, the only way to recover your wallet is from the seed phrase.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Remove wallet encryption
|
||||
ipcRenderer.on('decrypt', async () => {
|
||||
const { info, decryptWallet, openPassword } = this.props;
|
||||
|
||||
if (!info.encrypted) {
|
||||
openErrorModal('Not Encrypted', 'Your wallet is not encrypted and ready for spending.');
|
||||
} else {
|
||||
// Remove the wallet remove the wallet encryption
|
||||
openPassword(
|
||||
false,
|
||||
async password => {
|
||||
const success = await decryptWallet(password);
|
||||
if (success) {
|
||||
openErrorModal(
|
||||
'Decrypted',
|
||||
`Your wallet's encryption has been removed. A password will no longer be needed to spend funds.`
|
||||
);
|
||||
} else {
|
||||
openErrorModal('Decryption Failed', 'Wallet decryption failed. Do you have the right password?');
|
||||
}
|
||||
},
|
||||
() => {
|
||||
openErrorModal('Cancelled', 'Your wallet is still encrypted.');
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Unlock wallet
|
||||
ipcRenderer.on('unlock', () => {
|
||||
const { info } = this.props;
|
||||
if (!info.encrypted || !info.locked) {
|
||||
openErrorModal('Already Unlocked', 'Your wallet is already unlocked for spending');
|
||||
} else {
|
||||
openPasswordAndUnlockIfNeeded(async () => {
|
||||
openErrorModal('Unlocked', 'Your wallet is unlocked for spending');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Rescan
|
||||
ipcRenderer.on('rescan', () => {
|
||||
// To rescan, we reset the wallet loading
|
||||
// So set info the default, and redirect to the loading screen
|
||||
RPC.doRescan();
|
||||
|
||||
// Set the rescanning global state to true
|
||||
setRescanning(true);
|
||||
|
||||
// Reset the info object, it will be refetched
|
||||
setInfo(new Info());
|
||||
|
||||
history.push(routes.LOADING);
|
||||
});
|
||||
|
||||
// Export all private keys
|
||||
ipcRenderer.on('exportall', async () => {
|
||||
// Get all the addresses and run export key on each of them.
|
||||
const { addresses, getPrivKeyAsString } = this.props;
|
||||
openPasswordAndUnlockIfNeeded(async () => {
|
||||
const privKeysPromise = addresses.map(async a => {
|
||||
const privKey = await getPrivKeyAsString(a);
|
||||
return `${privKey} #${a}`;
|
||||
});
|
||||
const exportedPrivKeys = await Promise.all(privKeysPromise);
|
||||
|
||||
this.setState({ exportPrivKeysModalIsOpen: true, exportedPrivKeys });
|
||||
});
|
||||
});
|
||||
|
||||
// View zcashd
|
||||
ipcRenderer.on('zcashd', () => {
|
||||
history.push(routes.ZCASHD);
|
||||
});
|
||||
|
||||
// Connect mobile app
|
||||
ipcRenderer.on('connectmobile', () => {
|
||||
history.push(routes.CONNECTMOBILE);
|
||||
});
|
||||
};
|
||||
|
||||
closeExportPrivKeysModal = () => {
|
||||
this.setState({ exportPrivKeysModalIsOpen: false, exportedPrivKeys: null });
|
||||
};
|
||||
|
||||
openURIModal = (defaultValue: string | null) => {
|
||||
const uriModalInputValue = defaultValue || '';
|
||||
this.setState({ uriModalIsOpen: true, uriModalInputValue });
|
||||
};
|
||||
|
||||
setURIInputValue = (uriModalInputValue: string) => {
|
||||
this.setState({ uriModalInputValue });
|
||||
};
|
||||
|
||||
closeURIModal = () => {
|
||||
this.setState({ uriModalIsOpen: false });
|
||||
};
|
||||
|
||||
payURI = (uri: string) => {
|
||||
console.log(`Paying ${uri}`);
|
||||
const { openErrorModal, setSendTo, history } = this.props;
|
||||
|
||||
const errTitle = 'URI Error';
|
||||
const errBody = (
|
||||
<span>
|
||||
The URI "{escape(uri)}" was not recognized.
|
||||
<br />
|
||||
Please type in a valid URI of the form " zcash:address?amout=xx&memo=yy "
|
||||
</span>
|
||||
);
|
||||
|
||||
if (!uri || uri === '') {
|
||||
openErrorModal(errTitle, errBody);
|
||||
return;
|
||||
}
|
||||
|
||||
const parsedUri = url.parse(uri);
|
||||
if (!parsedUri || parsedUri.protocol !== 'zcash:' || !parsedUri.query) {
|
||||
openErrorModal(errTitle, errBody);
|
||||
return;
|
||||
}
|
||||
|
||||
const address = parsedUri.host;
|
||||
if (!address || !(Utils.isTransparent(address) || Utils.isZaddr(address))) {
|
||||
openErrorModal(errTitle, <span>The address ${address} was not recognized as a Zcash address</span>);
|
||||
return;
|
||||
}
|
||||
|
||||
const parsedParams = querystring.parse(parsedUri.query);
|
||||
if (!parsedParams || (!parsedParams.amt && !parsedParams.amount)) {
|
||||
openErrorModal(errTitle, errBody);
|
||||
return;
|
||||
}
|
||||
|
||||
const amount = parsedParams.amt || parsedParams.amount;
|
||||
const memo = parsedParams.memo || '';
|
||||
|
||||
setSendTo(address, amount, memo);
|
||||
history.push(routes.SEND);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { location, info } = this.props;
|
||||
const { uriModalIsOpen, uriModalInputValue, exportPrivKeysModalIsOpen, exportedPrivKeys } = this.state;
|
||||
|
||||
let state = 'DISCONNECTED';
|
||||
let progress = 100;
|
||||
if (info && info.version) {
|
||||
if (info.verificationProgress < 0.9999) {
|
||||
state = 'SYNCING';
|
||||
progress = (info.verificationProgress * 100).toFixed(1);
|
||||
} else {
|
||||
state = 'CONNECTED';
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Payment URI Modal */}
|
||||
<PayURIModal
|
||||
modalInput={uriModalInputValue}
|
||||
setModalInput={this.setURIInputValue}
|
||||
modalIsOpen={uriModalIsOpen}
|
||||
closeModal={this.closeURIModal}
|
||||
modalTitle="Pay URI"
|
||||
actionButtonName="Pay URI"
|
||||
actionCallback={this.payURI}
|
||||
/>
|
||||
|
||||
{/* Exported (all) Private Keys */}
|
||||
<ExportPrivKeyModal
|
||||
modalIsOpen={exportPrivKeysModalIsOpen}
|
||||
exportedPrivKeys={exportedPrivKeys}
|
||||
closeModal={this.closeExportPrivKeysModal}
|
||||
/>
|
||||
|
||||
<div className={[cstyles.center, styles.sidebarlogobg].join(' ')}>
|
||||
<img src={Logo} width="70" alt="logo" />
|
||||
</div>
|
||||
|
||||
<div className={styles.sidebar}>
|
||||
<SidebarMenuItem
|
||||
name="Dashboard"
|
||||
routeName={routes.DASHBOARD}
|
||||
currentRoute={location.pathname}
|
||||
iconname="fa-home"
|
||||
/>
|
||||
<SidebarMenuItem
|
||||
name="Send"
|
||||
routeName={routes.SEND}
|
||||
currentRoute={location.pathname}
|
||||
iconname="fa-paper-plane"
|
||||
/>
|
||||
<SidebarMenuItem
|
||||
name="Receive"
|
||||
routeName={routes.RECEIVE}
|
||||
currentRoute={location.pathname}
|
||||
iconname="fa-download"
|
||||
/>
|
||||
<SidebarMenuItem
|
||||
name="Transactions"
|
||||
routeName={routes.TRANSACTIONS}
|
||||
currentRoute={location.pathname}
|
||||
iconname="fa-list"
|
||||
/>
|
||||
<SidebarMenuItem
|
||||
name="Address Book"
|
||||
routeName={routes.ADDRESSBOOK}
|
||||
currentRoute={location.pathname}
|
||||
iconname="fa-address-book"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={cstyles.center}>
|
||||
{state === 'CONNECTED' && (
|
||||
<div className={[cstyles.padsmallall, cstyles.margintopsmall, cstyles.blackbg].join(' ')}>
|
||||
<i className={[cstyles.green, 'fas', 'fa-check'].join(' ')} />
|
||||
Connected
|
||||
</div>
|
||||
)}
|
||||
{state === 'SYNCING' && (
|
||||
<div className={[cstyles.padsmallall, cstyles.margintopsmall, cstyles.blackbg].join(' ')}>
|
||||
<div>
|
||||
<i className={[cstyles.yellow, 'fas', 'fa-sync'].join(' ')} />
|
||||
Syncing
|
||||
</div>
|
||||
<div>{`${progress}%`}</div>
|
||||
</div>
|
||||
)}
|
||||
{state === 'DISCONNECTED' && (
|
||||
<div className={[cstyles.padsmallall, cstyles.margintopsmall, cstyles.blackbg].join(' ')}>
|
||||
<i className={[cstyles.yellow, 'fas', 'fa-times-circle'].join(' ')} />
|
||||
Connected
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withRouter(Sidebar);
|
|
@ -0,0 +1,62 @@
|
|||
.txbox {
|
||||
margin-left: 16px;
|
||||
margin-right: 16px;
|
||||
margin-top: 4px;
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.txdate {
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.txtype {
|
||||
width: 15%;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.txaddressamount {
|
||||
width: 85%;
|
||||
float: left;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.txaddress {
|
||||
width: 75%;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.txamount {
|
||||
width: 20%;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.txmemo {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.txmodal {
|
||||
top: 12.5%;
|
||||
left: 12.5%;
|
||||
right: 12.5%;
|
||||
bottom: auto;
|
||||
opacity: 1;
|
||||
transform: translate(-0%, -12.5%);
|
||||
background: #212124;
|
||||
position: absolute;
|
||||
padding: 16px;
|
||||
min-width: 700px;
|
||||
max-height: calc(100vh - 100px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.txmodalOverlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(100, 100, 100, 0.5);
|
||||
}
|
|
@ -0,0 +1,323 @@
|
|||
/* eslint-disable react/prop-types */
|
||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||
import React, { Component } from 'react';
|
||||
import Modal from 'react-modal';
|
||||
import dateformat from 'dateformat';
|
||||
import { shell } from 'electron';
|
||||
import { withRouter } from 'react-router';
|
||||
import { BalanceBlockHighlight } from './BalanceBlocks';
|
||||
import styles from './Transactions.css';
|
||||
import cstyles from './Common.css';
|
||||
import { Transaction, Info } from './AppState';
|
||||
import ScrollPane from './ScrollPane';
|
||||
import Utils from '../utils/utils';
|
||||
import AddressBook from './Addressbook';
|
||||
import routes from '../constants/routes.json';
|
||||
|
||||
const TxModalInternal = ({ modalIsOpen, tx, closeModal, currencyName, zecPrice, setSendTo, history }) => {
|
||||
let txid = '';
|
||||
let type = '';
|
||||
let typeIcon = '';
|
||||
let typeColor = '';
|
||||
let confirmations = 0;
|
||||
let detailedTxns = [];
|
||||
let amount = 0;
|
||||
let datePart = '';
|
||||
let timePart = '';
|
||||
|
||||
if (tx) {
|
||||
txid = tx.txid;
|
||||
type = tx.type;
|
||||
if (tx.type === 'receive') {
|
||||
typeIcon = 'fa-arrow-circle-down';
|
||||
typeColor = 'green';
|
||||
} else {
|
||||
typeIcon = 'fa-arrow-circle-up';
|
||||
typeColor = 'red';
|
||||
}
|
||||
|
||||
datePart = dateformat(tx.time * 1000, 'mmm dd, yyyy');
|
||||
timePart = dateformat(tx.time * 1000, 'hh:MM tt');
|
||||
|
||||
confirmations = tx.confirmations;
|
||||
detailedTxns = tx.detailedTxns;
|
||||
amount = Math.abs(tx.amount);
|
||||
}
|
||||
|
||||
const openTxid = () => {
|
||||
if (currencyName === 'TAZ') {
|
||||
shell.openExternal(`https://chain.so/tx/ZECTEST/${txid}`);
|
||||
} else {
|
||||
shell.openExternal(`https://zcha.in/transactions/${txid}`);
|
||||
}
|
||||
};
|
||||
|
||||
const doReply = (address: string) => {
|
||||
setSendTo(address, 0.0001, null);
|
||||
closeModal();
|
||||
|
||||
history.push(routes.SEND);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={modalIsOpen}
|
||||
onRequestClose={closeModal}
|
||||
className={styles.txmodal}
|
||||
overlayClassName={styles.txmodalOverlay}
|
||||
>
|
||||
<div className={[cstyles.verticalflex].join(' ')}>
|
||||
<div className={[cstyles.marginbottomlarge, cstyles.center].join(' ')}>Transaction Status</div>
|
||||
|
||||
<div className={[cstyles.center].join(' ')}>
|
||||
<i className={['fas', typeIcon].join(' ')} style={{ fontSize: '96px', color: typeColor }} />
|
||||
</div>
|
||||
|
||||
<div className={[cstyles.center].join(' ')}>
|
||||
{type}
|
||||
<BalanceBlockHighlight
|
||||
zecValue={amount}
|
||||
usdValue={Utils.getZecToUsdString(zecPrice, Math.abs(amount))}
|
||||
currencyName={currencyName}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={[cstyles.flexspacebetween].join(' ')}>
|
||||
<div>
|
||||
<div className={[cstyles.sublight].join(' ')}>Time</div>
|
||||
<div>
|
||||
{datePart} {timePart}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className={[cstyles.sublight].join(' ')}>Confirmations</div>
|
||||
<div>{confirmations}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={cstyles.margintoplarge} />
|
||||
|
||||
<div className={[cstyles.flexspacebetween].join(' ')}>
|
||||
<div>
|
||||
<div className={[cstyles.sublight].join(' ')}>TXID</div>
|
||||
<div>{txid}</div>
|
||||
</div>
|
||||
|
||||
<div className={cstyles.primarybutton} onClick={openTxid}>
|
||||
View TXID
|
||||
<i className={['fas', 'fa-external-link-square-alt'].join(' ')} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={cstyles.margintoplarge} />
|
||||
<hr />
|
||||
|
||||
{detailedTxns.map(txdetail => {
|
||||
const { bigPart, smallPart } = Utils.splitZecAmountIntoBigSmall(Math.abs(txdetail.amount));
|
||||
|
||||
let { address } = txdetail;
|
||||
const { memo } = txdetail;
|
||||
|
||||
if (!address) {
|
||||
address = '(Shielded)';
|
||||
}
|
||||
|
||||
let replyTo = null;
|
||||
if (tx.type === 'receive' && memo) {
|
||||
const split = memo.split(/[ :\n\r\t]+/);
|
||||
console.log(split);
|
||||
if (split && split.length > 0 && Utils.isSapling(split[split.length - 1])) {
|
||||
replyTo = split[split.length - 1];
|
||||
}
|
||||
}
|
||||
|
||||
console.log('replyto is', replyTo);
|
||||
|
||||
return (
|
||||
<div key={address} className={cstyles.verticalflex}>
|
||||
<div className={[cstyles.sublight].join(' ')}>Address</div>
|
||||
<div>{Utils.splitStringIntoChunks(address, 6).join(' ')}</div>
|
||||
|
||||
<div className={cstyles.margintoplarge} />
|
||||
|
||||
<div className={[cstyles.sublight].join(' ')}>Amount</div>
|
||||
<div>
|
||||
<span>
|
||||
{currencyName} {bigPart}
|
||||
</span>
|
||||
<span className={[cstyles.small, cstyles.zecsmallpart].join(' ')}>{smallPart}</span>
|
||||
</div>
|
||||
|
||||
<div className={cstyles.margintoplarge} />
|
||||
|
||||
{memo && (
|
||||
<div>
|
||||
<div className={[cstyles.sublight].join(' ')}>Memo</div>
|
||||
<div className={[cstyles.flexspacebetween].join(' ')}>
|
||||
<div>{memo}</div>
|
||||
{replyTo && (
|
||||
<div className={cstyles.primarybutton} onClick={() => doReply(replyTo)}>
|
||||
Reply
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<hr />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
<div className={[cstyles.center, cstyles.margintoplarge].join(' ')}>
|
||||
<button type="button" className={cstyles.primarybutton} onClick={closeModal}>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const TxModal = withRouter(TxModalInternal);
|
||||
|
||||
const TxItemBlock = ({ transaction, currencyName, zecPrice, txClicked, addressBookMap }) => {
|
||||
const txDate = new Date(transaction.time * 1000);
|
||||
const datePart = dateformat(txDate, 'mmm dd, yyyy');
|
||||
const timePart = dateformat(txDate, 'hh:MM tt');
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={[cstyles.small, cstyles.sublight, styles.txdate].join(' ')}>{datePart}</div>
|
||||
<div
|
||||
className={[cstyles.well, styles.txbox].join(' ')}
|
||||
onClick={() => {
|
||||
txClicked(transaction);
|
||||
}}
|
||||
>
|
||||
<div className={styles.txtype}>
|
||||
<div>{transaction.type}</div>
|
||||
<div className={[cstyles.padtopsmall, cstyles.sublight].join(' ')}>{timePart}</div>
|
||||
</div>
|
||||
<div className={styles.txaddressamount}>
|
||||
{transaction.detailedTxns.map(txdetail => {
|
||||
const { bigPart, smallPart } = Utils.splitZecAmountIntoBigSmall(Math.abs(txdetail.amount));
|
||||
|
||||
let { address } = txdetail;
|
||||
const { memo } = txdetail;
|
||||
|
||||
if (!address) {
|
||||
address = '(Shielded)';
|
||||
}
|
||||
|
||||
const label = addressBookMap[address] || '';
|
||||
|
||||
return (
|
||||
<div key={address} className={cstyles.padtopsmall}>
|
||||
<div className={styles.txaddress}>
|
||||
<div className={cstyles.highlight}>{label}</div>
|
||||
<div>{Utils.splitStringIntoChunks(address, 6).join(' ')}</div>
|
||||
<div className={[cstyles.small, cstyles.sublight, cstyles.padtopsmall, styles.txmemo].join(' ')}>
|
||||
{memo}
|
||||
</div>
|
||||
</div>
|
||||
<div className={[styles.txamount, cstyles.right].join(' ')}>
|
||||
<div>
|
||||
<span>
|
||||
{currencyName} {bigPart}
|
||||
</span>
|
||||
<span className={[cstyles.small, cstyles.zecsmallpart].join(' ')}>{smallPart}</span>
|
||||
</div>
|
||||
<div className={[cstyles.sublight, cstyles.small, cstyles.padtopsmall].join(' ')}>
|
||||
{Utils.getZecToUsdString(zecPrice, Math.abs(txdetail.amount))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type Props = {
|
||||
transactions: Transaction[],
|
||||
addressBook: AddressBook[],
|
||||
info: Info,
|
||||
setSendTo: (string, number | null, string | null) => void
|
||||
};
|
||||
|
||||
type State = {
|
||||
clickedTx: Transaction | null,
|
||||
modalIsOpen: boolean
|
||||
};
|
||||
|
||||
export default class Transactions extends Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = { clickedTx: null, modalIsOpen: false };
|
||||
}
|
||||
|
||||
txClicked = (tx: Transaction) => {
|
||||
// Show the modal
|
||||
if (!tx) return;
|
||||
this.setState({ clickedTx: tx, modalIsOpen: true });
|
||||
};
|
||||
|
||||
closeModal = () => {
|
||||
this.setState({ clickedTx: null, modalIsOpen: false });
|
||||
};
|
||||
|
||||
render() {
|
||||
const { transactions, info, addressBook, setSendTo } = this.props;
|
||||
const { clickedTx, modalIsOpen } = this.state;
|
||||
|
||||
const addressBookMap = addressBook.reduce((map, obj) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
map[obj.address] = obj.label;
|
||||
return map;
|
||||
}, {});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={[cstyles.xlarge, cstyles.padall, cstyles.center].join(' ')}>Transactions</div>
|
||||
|
||||
{/* Change the hardcoded height */}
|
||||
<ScrollPane offsetHeight={100}>
|
||||
{/* If no transactions, show the "loading..." text */
|
||||
!transactions && <div className={[cstyles.center, cstyles.margintoplarge].join(' ')}>Loading...</div>}
|
||||
|
||||
{transactions && transactions.length === 0 && (
|
||||
<div className={[cstyles.center, cstyles.margintoplarge].join(' ')}>No Transactions Yet</div>
|
||||
)}
|
||||
{transactions &&
|
||||
transactions.map(t => {
|
||||
const key = t.type + t.txid + t.address;
|
||||
return (
|
||||
<TxItemBlock
|
||||
key={key}
|
||||
transaction={t}
|
||||
currencyName={info.currencyName}
|
||||
zecPrice={info.zecPrice}
|
||||
txClicked={this.txClicked}
|
||||
addressBookMap={addressBookMap}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ScrollPane>
|
||||
|
||||
<TxModal
|
||||
modalIsOpen={modalIsOpen}
|
||||
tx={clickedTx}
|
||||
closeModal={this.closeModal}
|
||||
currencyName={info.currencyName}
|
||||
zecPrice={info.zecPrice}
|
||||
setSendTo={setSendTo}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
.wormholeqr {
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
.qrcodecontainer {
|
||||
margin: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.appinfocontainer {
|
||||
width: 50%;
|
||||
margin-left: 25%;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.appinfo {
|
||||
margin-top: 20px;
|
||||
border: 1px solid grey;
|
||||
padding: 16px;
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
import React, { PureComponent } from 'react';
|
||||
import QRCode from 'qrcode.react';
|
||||
import dateformat from 'dateformat';
|
||||
import cstyles from './Common.css';
|
||||
import styles from './WormholeConnection.css';
|
||||
import CompanionAppListener from '../companion';
|
||||
import { ConnectedCompanionApp } from './AppState';
|
||||
|
||||
type Props = {
|
||||
companionAppListener: CompanionAppListener,
|
||||
connectedCompanionApp: ConnectedCompanionApp | null
|
||||
};
|
||||
|
||||
type State = {
|
||||
tempKeyHex: string
|
||||
};
|
||||
|
||||
export default class WormholeConnection extends PureComponent<Props, State> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { tempKeyHex: null };
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// If there is no temp key, create one
|
||||
const { companionAppListener } = this.props;
|
||||
const { tempKeyHex } = this.state;
|
||||
|
||||
if (!tempKeyHex) {
|
||||
const newKey = companionAppListener.genNewKeyHex();
|
||||
companionAppListener.createTmpClient(newKey);
|
||||
this.setState({ tempKeyHex: newKey });
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
const { companionAppListener } = this.props;
|
||||
companionAppListener.closeTmpClient();
|
||||
}
|
||||
|
||||
disconnectCurrentMobile = () => {
|
||||
const { companionAppListener } = this.props;
|
||||
companionAppListener.disconnectLastClient();
|
||||
};
|
||||
|
||||
render() {
|
||||
const { tempKeyHex } = this.state;
|
||||
const { connectedCompanionApp } = this.props;
|
||||
|
||||
const clientName = (connectedCompanionApp && connectedCompanionApp.name) || null;
|
||||
const lastSeen = (connectedCompanionApp && connectedCompanionApp.lastSeen) || null;
|
||||
|
||||
let datePart = null;
|
||||
let timePart = null;
|
||||
|
||||
if (lastSeen) {
|
||||
const txDate = new Date(lastSeen);
|
||||
datePart = dateformat(txDate, 'mmm dd, yyyy');
|
||||
timePart = dateformat(txDate, 'hh:MM tt');
|
||||
}
|
||||
|
||||
const connStr = `ws://127.0.0.1:7070,${tempKeyHex},1`;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={[cstyles.xlarge, cstyles.padall, cstyles.center].join(' ')}>Connect Mobile App</div>
|
||||
|
||||
<div className={styles.qrcodecontainer}>
|
||||
<div>This is your connection code. Scan this QR code from the Zecwallet Companion App.</div>
|
||||
|
||||
<div className={[cstyles.center, cstyles.margintoplarge].join(' ')}>
|
||||
<QRCode value={connStr} size={256} className={styles.wormholeqr} />
|
||||
</div>
|
||||
<div className={[cstyles.sublight, cstyles.margintoplarge, cstyles.small].join(' ')}>{connStr}</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.appinfocontainer}>
|
||||
<div className={styles.appinfo}>
|
||||
{clientName && (
|
||||
<div>
|
||||
<div className={cstyles.flexspacebetween}>
|
||||
<div style={{ flex: 1 }} className={cstyles.sublight}>
|
||||
Current App Connected:
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>{clientName}</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex' }}>
|
||||
<div style={{ flex: 1 }} className={cstyles.sublight}>
|
||||
Last Seen:
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
{datePart} {timePart}
|
||||
</div>
|
||||
</div>
|
||||
<div className={cstyles.margintoplarge}>
|
||||
<button type="button" className={cstyles.primarybutton} onClick={this.disconnectCurrentMobile}>
|
||||
Disconnect
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!clientName && <div>No Companion App Connected</div>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
.container {
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.imgcontainer {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
margin-top: 10%;
|
||||
}
|
||||
|
||||
.imgcontainer img {
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.detailcontainer {
|
||||
width: 50%;
|
||||
flex-direction: column;
|
||||
margin-left: 25%;
|
||||
margin-top: 5%;
|
||||
}
|
||||
|
||||
.detaillines {
|
||||
border: 1px solid grey;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.detailline {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
/* eslint-disable react/prop-types */
|
||||
import React, { Component } from 'react';
|
||||
import { Info } from './AppState';
|
||||
import cstyles from './Common.css';
|
||||
import styles from './Zcashd.css';
|
||||
import ScrollPane from './ScrollPane';
|
||||
import Heart from '../assets/img/zcashdlogo.gif';
|
||||
|
||||
const DetailLine = ({ label, value }) => {
|
||||
return (
|
||||
<div className={styles.detailline}>
|
||||
<div className={cstyles.sublight}>{label} :</div>
|
||||
<div>{value}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type Props = {
|
||||
info: Info,
|
||||
refresh: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default class Zcashd extends Component<Props> {
|
||||
render() {
|
||||
const { info, rpcConfig, refresh } = this.props;
|
||||
const { url } = rpcConfig;
|
||||
|
||||
if (!info || !info.version) {
|
||||
return (
|
||||
<div>
|
||||
<div className={[cstyles.verticalflex, cstyles.center].join(' ')}>
|
||||
<div style={{ marginTop: '100px' }}>
|
||||
<i className={['fas', 'fa-times-circle'].join(' ')} style={{ fontSize: '96px', color: 'red' }} />
|
||||
</div>
|
||||
<div className={cstyles.margintoplarge}>Not Connected</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
// eslint-disable-next-line no-else-return
|
||||
} else {
|
||||
let height = info.latestBlock;
|
||||
if (info.verificationProgress < 0.9999) {
|
||||
const progress = (info.verificationProgress * 100).toFixed(1);
|
||||
height = `${height} (${progress}%)`;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.container}>
|
||||
<ScrollPane offsetHeight={0}>
|
||||
<div className={styles.imgcontainer}>
|
||||
<img src={Heart} alt="heart" />
|
||||
</div>
|
||||
|
||||
<div className={styles.detailcontainer}>
|
||||
<div className={styles.detaillines}>
|
||||
<DetailLine label="version" value={info.version} />
|
||||
<DetailLine label="Lightwallet Server" value={url} />
|
||||
<DetailLine label="Network" value={info.testnet ? 'Testnet' : 'Mainnet'} />
|
||||
<DetailLine label="Block Height" value={height} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={cstyles.buttoncontainer}>
|
||||
<button className={cstyles.primarybutton} type="button" onClick={refresh}>
|
||||
Refresh All Data
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className={cstyles.margintoplarge} />
|
||||
</ScrollPane>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"LOADING": "/",
|
||||
"DASHBOARD": "/dashboard",
|
||||
"SEND": "/send",
|
||||
"RECEIVE": "/receive",
|
||||
"ADDRESSBOOK": "/addressbook",
|
||||
"TRANSACTIONS": "/transactions",
|
||||
"SETTINGS": "/settings",
|
||||
"ZCASHD": "/zcashd",
|
||||
"CONNECTMOBILE": "/connectmobile"
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
// @flow
|
||||
import * as React from 'react';
|
||||
|
||||
type Props = {
|
||||
children: React.Node
|
||||
};
|
||||
|
||||
export default class App extends React.Component<Props> {
|
||||
props: Props;
|
||||
|
||||
render() {
|
||||
const { children } = this.props;
|
||||
return <>{children}</>;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import { hot } from 'react-hot-loader/root';
|
||||
import Routes from '../Routes';
|
||||
|
||||
const Root = () => (
|
||||
<Router>
|
||||
<Routes />
|
||||
</Router>
|
||||
);
|
||||
|
||||
export default hot(Root);
|
|
@ -0,0 +1,14 @@
|
|||
import React, { Fragment } from 'react';
|
||||
import { render } from 'react-dom';
|
||||
import { AppContainer as ReactHotAppContainer } from 'react-hot-loader';
|
||||
import Root from './containers/Root';
|
||||
import './app.global.css';
|
||||
|
||||
const AppContainer = process.env.PLAIN_HMR ? Fragment : ReactHotAppContainer;
|
||||
|
||||
render(
|
||||
<AppContainer>
|
||||
<Root />
|
||||
</AppContainer>,
|
||||
document.getElementById('root')
|
||||
);
|
|
@ -0,0 +1,152 @@
|
|||
/* eslint-disable compat/compat */
|
||||
/* eslint global-require: off */
|
||||
|
||||
/**
|
||||
* This module executes inside of electron's main process. You can start
|
||||
* electron renderer process from here and communicate with the other processes
|
||||
* through IPC.
|
||||
*
|
||||
* When running `yarn build` or `yarn build-main`, this file is compiled to
|
||||
* `./app/main.prod.js` using webpack. This gives us some performance wins.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
import { app, shell, BrowserWindow, ipcMain } from 'electron';
|
||||
import log from 'electron-log';
|
||||
import MenuBuilder from './menu';
|
||||
|
||||
export default class AppUpdater {
|
||||
constructor() {
|
||||
log.transports.file.level = 'info';
|
||||
}
|
||||
}
|
||||
|
||||
let mainWindow = null;
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
const sourceMapSupport = require('source-map-support');
|
||||
sourceMapSupport.install();
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true') {
|
||||
require('electron-debug')();
|
||||
}
|
||||
|
||||
const installExtensions = async () => {
|
||||
const installer = require('electron-devtools-installer');
|
||||
const forceDownload = !!process.env.UPGRADE_EXTENSIONS;
|
||||
const extensions = ['REACT_DEVELOPER_TOOLS', 'REDUX_DEVTOOLS'];
|
||||
|
||||
return Promise.all(extensions.map(name => installer.default(installer[name], forceDownload))).catch(console.log);
|
||||
};
|
||||
|
||||
let waitingForClose = false;
|
||||
let proceedToClose = false;
|
||||
|
||||
const createWindow = async () => {
|
||||
if (process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true') {
|
||||
await installExtensions();
|
||||
}
|
||||
|
||||
mainWindow = new BrowserWindow({
|
||||
show: false,
|
||||
width: 1300,
|
||||
height: 728,
|
||||
minHeight: 500,
|
||||
minWidth: 1100,
|
||||
webPreferences: {
|
||||
// Allow node integration because we're only loading local content here.
|
||||
nodeIntegration: true
|
||||
}
|
||||
});
|
||||
|
||||
mainWindow.loadURL(`file://${__dirname}/app.html`);
|
||||
|
||||
app.on('web-contents-created', (event, contents) => {
|
||||
contents.on('new-window', async (eventInner, navigationUrl) => {
|
||||
// In this example, we'll ask the operating system
|
||||
// to open this event's url in the default browser.
|
||||
console.log('attempting to open window', navigationUrl);
|
||||
|
||||
eventInner.preventDefault();
|
||||
|
||||
await shell.openExternal(navigationUrl);
|
||||
});
|
||||
});
|
||||
|
||||
// @TODO: Use 'ready-to-show' event
|
||||
// https://github.com/electron/electron/blob/master/docs/api/browser-window.md#using-ready-to-show-event
|
||||
mainWindow.webContents.on('did-finish-load', () => {
|
||||
if (!mainWindow) {
|
||||
throw new Error('"mainWindow" is not defined');
|
||||
}
|
||||
if (process.env.START_MINIMIZED) {
|
||||
mainWindow.minimize();
|
||||
} else {
|
||||
mainWindow.show();
|
||||
mainWindow.focus();
|
||||
}
|
||||
});
|
||||
|
||||
mainWindow.on('close', event => {
|
||||
// If we are clear to close, then return and allow everything to close
|
||||
if (proceedToClose) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If we're already waiting for close, then don't allow another close event to actually close the window
|
||||
if (waitingForClose) {
|
||||
console.log('Waiting for close... Timeout in 10s');
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
waitingForClose = true;
|
||||
event.preventDefault();
|
||||
|
||||
ipcMain.on('appquitdone', () => {
|
||||
waitingForClose = false;
|
||||
proceedToClose = true;
|
||||
app.quit();
|
||||
});
|
||||
|
||||
// $FlowFixMe
|
||||
mainWindow.webContents.send('appquitting');
|
||||
|
||||
// Failsafe, timeout after 3 seconds
|
||||
setTimeout(() => {
|
||||
waitingForClose = false;
|
||||
proceedToClose = true;
|
||||
console.log('Timeout, quitting');
|
||||
|
||||
app.quit();
|
||||
}, 3 * 1000);
|
||||
});
|
||||
|
||||
mainWindow.on('closed', () => {
|
||||
mainWindow = null;
|
||||
});
|
||||
|
||||
const menuBuilder = new MenuBuilder(mainWindow);
|
||||
menuBuilder.buildMenu();
|
||||
|
||||
// Remove this if your app does not use auto updates
|
||||
// eslint-disable-next-line
|
||||
new AppUpdater();
|
||||
};
|
||||
|
||||
/**
|
||||
* Add event listeners...
|
||||
*/
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
app.quit();
|
||||
});
|
||||
|
||||
app.on('ready', createWindow);
|
||||
|
||||
app.on('activate', () => {
|
||||
// On macOS it's common to re-create a window in the app when the
|
||||
// dock icon is clicked and there are no other windows open.
|
||||
if (mainWindow === null) createWindow();
|
||||
});
|
|
@ -0,0 +1 @@
|
|||
/*! http://mths.be/fromcodepoint v0.1.0 by @mathias */
|
|
@ -0,0 +1,367 @@
|
|||
// @flow
|
||||
import { app, Menu, shell, BrowserWindow } from 'electron';
|
||||
|
||||
export default class MenuBuilder {
|
||||
mainWindow: BrowserWindow;
|
||||
|
||||
constructor(mainWindow: BrowserWindow) {
|
||||
this.mainWindow = mainWindow;
|
||||
}
|
||||
|
||||
buildMenu() {
|
||||
if (process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true') {
|
||||
this.setupDevelopmentEnvironment();
|
||||
}
|
||||
|
||||
const template = process.platform === 'darwin' ? this.buildDarwinTemplate() : this.buildDefaultTemplate();
|
||||
|
||||
const menu = Menu.buildFromTemplate(template);
|
||||
Menu.setApplicationMenu(menu);
|
||||
|
||||
return menu;
|
||||
}
|
||||
|
||||
setupDevelopmentEnvironment() {
|
||||
this.mainWindow.openDevTools();
|
||||
this.mainWindow.webContents.on('context-menu', (e, props) => {
|
||||
const { x, y } = props;
|
||||
|
||||
Menu.buildFromTemplate([
|
||||
{
|
||||
label: 'Inspect element',
|
||||
click: () => {
|
||||
this.mainWindow.inspectElement(x, y);
|
||||
}
|
||||
}
|
||||
]).popup(this.mainWindow);
|
||||
});
|
||||
}
|
||||
|
||||
buildDarwinTemplate() {
|
||||
const { mainWindow } = this;
|
||||
|
||||
const subMenuAbout = {
|
||||
label: 'Zecwallet Lite',
|
||||
submenu: [
|
||||
{
|
||||
label: 'About Zecwallet Lite',
|
||||
selector: 'orderFrontStandardAboutPanel:',
|
||||
click: () => {
|
||||
mainWindow.webContents.send('about');
|
||||
}
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{ label: 'Services', submenu: [] },
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Hide Zecwallet Lite',
|
||||
accelerator: 'Command+H',
|
||||
selector: 'hide:'
|
||||
},
|
||||
{
|
||||
label: 'Hide Others',
|
||||
accelerator: 'Command+Shift+H',
|
||||
selector: 'hideOtherApplications:'
|
||||
},
|
||||
{ label: 'Show All', selector: 'unhideAllApplications:' },
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Quit',
|
||||
accelerator: 'Command+Q',
|
||||
click: () => {
|
||||
app.quit();
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
const subMenuEdit = {
|
||||
label: 'Edit',
|
||||
submenu: [
|
||||
{ label: 'Undo', accelerator: 'Command+Z', selector: 'undo:' },
|
||||
{ label: 'Redo', accelerator: 'Shift+Command+Z', selector: 'redo:' },
|
||||
{ type: 'separator' },
|
||||
{ label: 'Cut', accelerator: 'Command+X', selector: 'cut:' },
|
||||
{ label: 'Copy', accelerator: 'Command+C', selector: 'copy:' },
|
||||
{ label: 'Paste', accelerator: 'Command+V', selector: 'paste:' },
|
||||
{
|
||||
label: 'Select All',
|
||||
accelerator: 'Command+A',
|
||||
selector: 'selectAll:'
|
||||
}
|
||||
]
|
||||
};
|
||||
const subMenuViewDev = {
|
||||
label: 'Wallet',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Wallet Seed',
|
||||
click: () => {
|
||||
mainWindow.webContents.send('seed');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '&Export All Private Keys',
|
||||
click: () => {
|
||||
mainWindow.webContents.send('exportall');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '&Rescan',
|
||||
click: () => {
|
||||
mainWindow.webContents.send('rescan');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'View Lightwalletd Info',
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('zcashd');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Connect Mobile App',
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('connectmobile');
|
||||
}
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Encrypt Wallet',
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('encrypt');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Remove Wallet Encryption',
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('decrypt');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Unlock',
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('unlock');
|
||||
}
|
||||
}
|
||||
// { type: 'separator' },
|
||||
// {
|
||||
// label: 'Toggle Developer Tools',
|
||||
// accelerator: 'Alt+Command+I',
|
||||
// click: () => {
|
||||
// this.mainWindow.toggleDevTools();
|
||||
// }
|
||||
// }
|
||||
]
|
||||
};
|
||||
const subMenuViewProd = {
|
||||
label: 'Wallet',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Wallet Seed',
|
||||
click: () => {
|
||||
mainWindow.webContents.send('seed');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '&Export All Private Keys',
|
||||
click: () => {
|
||||
mainWindow.webContents.send('exportall');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '&Rescan',
|
||||
click: () => {
|
||||
mainWindow.webContents.send('rescan');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Server info',
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('zcashd');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Connect Mobile App',
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('connectmobile');
|
||||
}
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Encrypt Wallet',
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('encrypt');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Remove Wallet Encryption',
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('decrypt');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Unlock',
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('unlock');
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
const subMenuWindow = {
|
||||
label: 'Window',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Minimize',
|
||||
accelerator: 'Command+M',
|
||||
selector: 'performMiniaturize:'
|
||||
},
|
||||
{ label: 'Close', accelerator: 'Command+W', selector: 'performClose:' },
|
||||
{ type: 'separator' },
|
||||
{ label: 'Bring All to Front', selector: 'arrangeInFront:' }
|
||||
]
|
||||
};
|
||||
const subMenuHelp = {
|
||||
label: 'Help',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Donate',
|
||||
click() {
|
||||
mainWindow.webContents.send('donate');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Check github.com for updates',
|
||||
click() {
|
||||
shell.openExternal('https://github.com/adityapk00/zecwallet-lite/releases');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'File a bug...',
|
||||
click() {
|
||||
shell.openExternal('https://github.com/adityapk00/zecwallet-lite/issues');
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const subMenuView = process.env.NODE_ENV === 'development' ? subMenuViewDev : subMenuViewProd;
|
||||
|
||||
return [subMenuAbout, subMenuEdit, subMenuView, subMenuWindow, subMenuHelp];
|
||||
}
|
||||
|
||||
buildDefaultTemplate() {
|
||||
const { mainWindow } = this;
|
||||
|
||||
const templateDefault = [
|
||||
{
|
||||
label: '&File',
|
||||
submenu: [
|
||||
{
|
||||
label: '&Pay URI',
|
||||
accelerator: 'Ctrl+P',
|
||||
click: () => {
|
||||
mainWindow.webContents.send('payuri');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '&Close',
|
||||
accelerator: 'Ctrl+W',
|
||||
click: () => {
|
||||
this.mainWindow.close();
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '&Wallet',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Wallet Seed',
|
||||
click: () => {
|
||||
mainWindow.webContents.send('seed');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '&Export All Private Keys',
|
||||
click: () => {
|
||||
mainWindow.webContents.send('exportall');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '&Rescan',
|
||||
click: () => {
|
||||
mainWindow.webContents.send('rescan');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Server info',
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('zcashd');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Connect Mobile App',
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('connectmobile');
|
||||
}
|
||||
},
|
||||
// {
|
||||
// label: 'Devtools',
|
||||
// click: () => {
|
||||
// mainWindow.webContents.openDevTools();
|
||||
// }
|
||||
// },
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Encrypt Wallet',
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('encrypt');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Remove Wallet Encryption',
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('decrypt');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Unlock',
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('unlock');
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Help',
|
||||
submenu: [
|
||||
{
|
||||
label: 'About Zecwallet Lite',
|
||||
click: () => {
|
||||
mainWindow.webContents.send('about');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Donate',
|
||||
click() {
|
||||
mainWindow.webContents.send('donate');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Check github.com for updates',
|
||||
click() {
|
||||
shell.openExternal('https://github.com/adityapk00/zecwallet-lite/releases');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'File a bug...',
|
||||
click() {
|
||||
shell.openExternal('https://github.com/adityapk00/zecwallet-lite/issues');
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
return templateDefault;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"name": "electron-react-boilerplate",
|
||||
"version": "0.18.1",
|
||||
"lockfileVersion": 1
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"name": "zecwallet",
|
||||
"productName": "Zecwallet Lite",
|
||||
"version": "1.1.0-beta1",
|
||||
"description": "Zecwallet Lite",
|
||||
"main": "./main.prod.js",
|
||||
"author": {
|
||||
"name": "Aditya Kulkarni",
|
||||
"email": "aditya@zecwallet.co",
|
||||
"url": "https://github.com/adityapk00/zecwallet-electron"
|
||||
},
|
||||
"scripts": {
|
||||
"electron-rebuild": "node -r ../internals/scripts/BabelRegister.js ../internals/scripts/ElectronRebuild.js",
|
||||
"postinstall": "yarn electron-rebuild"
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {}
|
||||
}
|
|
@ -0,0 +1,379 @@
|
|||
/* eslint-disable max-classes-per-file */
|
||||
import axios from 'axios';
|
||||
import { TotalBalance, AddressBalance, Transaction, RPCConfig, TxDetail, Info } from './components/AppState';
|
||||
import native from '../native/index.node';
|
||||
|
||||
export default class RPC {
|
||||
rpcConfig: RPCConfig;
|
||||
|
||||
fnSetInfo: Info => void;
|
||||
|
||||
fnSetTotalBalance: TotalBalance => void;
|
||||
|
||||
fnSetAddressesWithBalance: (AddressBalance[]) => void;
|
||||
|
||||
fnSetTransactionsList: (Transaction[]) => void;
|
||||
|
||||
fnSetAllAddresses: (string[]) => void;
|
||||
|
||||
fnSetZecPrice: number => void;
|
||||
|
||||
refreshTimerID: TimerID;
|
||||
|
||||
priceTimerID: TimerID;
|
||||
|
||||
constructor(
|
||||
fnSetTotalBalance: TotalBalance => void,
|
||||
fnSetAddressesWithBalance: (AddressBalance[]) => void,
|
||||
fnSetTransactionsList: (Transaction[]) => void,
|
||||
fnSetAllAddresses: (string[]) => void,
|
||||
fnSetInfo: Info => void,
|
||||
fnSetZecPrice: number => void
|
||||
) {
|
||||
this.fnSetTotalBalance = fnSetTotalBalance;
|
||||
this.fnSetAddressesWithBalance = fnSetAddressesWithBalance;
|
||||
this.fnSetTransactionsList = fnSetTransactionsList;
|
||||
this.fnSetAllAddresses = fnSetAllAddresses;
|
||||
this.fnSetInfo = fnSetInfo;
|
||||
this.fnSetZecPrice = fnSetZecPrice;
|
||||
}
|
||||
|
||||
async configure(rpcConfig: RPCConfig) {
|
||||
this.rpcConfig = rpcConfig;
|
||||
|
||||
if (!this.refreshTimerID) {
|
||||
this.refreshTimerID = setTimeout(() => this.refresh(), 1000);
|
||||
}
|
||||
|
||||
if (!this.priceTimerID) {
|
||||
this.priceTimerID = setTimeout(() => this.getZecPrice(), 1000);
|
||||
}
|
||||
}
|
||||
|
||||
setupNextFetch(lastBlockHeight: number) {
|
||||
this.refreshTimerID = setTimeout(() => this.refresh(lastBlockHeight), 60 * 1000);
|
||||
}
|
||||
|
||||
static doSync() {
|
||||
const syncstr = native.litelib_execute('sync', '');
|
||||
console.log(`Sync exec result: ${syncstr}`);
|
||||
}
|
||||
|
||||
static doRescan() {
|
||||
const syncstr = native.litelib_execute('rescan', '');
|
||||
console.log(`rescan exec result: ${syncstr}`);
|
||||
}
|
||||
|
||||
static doSyncStatus(): string {
|
||||
const syncstr = native.litelib_execute('syncstatus', '');
|
||||
console.log(`syncstatus: ${syncstr}`);
|
||||
return syncstr;
|
||||
}
|
||||
|
||||
static doSave() {
|
||||
const savestr = native.litelib_execute('save', '');
|
||||
console.log(`Sync status: ${savestr}`);
|
||||
}
|
||||
|
||||
async refresh(lastBlockHeight: number) {
|
||||
const latestBlockHeight = await this.fetchInfo();
|
||||
|
||||
if (!lastBlockHeight || lastBlockHeight < latestBlockHeight) {
|
||||
// If the latest block height has changed, make sure to sync
|
||||
await RPC.doSync();
|
||||
|
||||
const balP = this.fetchTotalBalance();
|
||||
const txns = this.fetchTandZTransactions(latestBlockHeight);
|
||||
|
||||
await balP;
|
||||
await txns;
|
||||
|
||||
// All done, set up next fetch
|
||||
console.log(`Finished full refresh at ${latestBlockHeight}`);
|
||||
} else {
|
||||
// Still at the latest block
|
||||
console.log('Already have latest block, waiting for next refresh');
|
||||
}
|
||||
|
||||
this.setupNextFetch(latestBlockHeight);
|
||||
}
|
||||
|
||||
// Special method to get the Info object. This is used both internally and by the Loading screen
|
||||
static getInfoObject() {
|
||||
const infostr = native.litelib_execute('info', '');
|
||||
const infoJSON = JSON.parse(infostr);
|
||||
|
||||
const info = new Info();
|
||||
info.testnet = infoJSON.chain_name === 'test';
|
||||
info.latestBlock = infoJSON.latest_block_height;
|
||||
info.connections = 1;
|
||||
info.version = infoJSON.version;
|
||||
info.verificationProgress = 1;
|
||||
info.currencyName = info.testnet ? 'TAZ' : 'ZEC';
|
||||
info.solps = 0;
|
||||
|
||||
const encStatus = native.litelib_execute('encryptionstatus', '');
|
||||
const encJSON = JSON.parse(encStatus);
|
||||
info.encrypted = encJSON.encrypted;
|
||||
info.locked = encJSON.locked;
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
async fetchInfo(): number {
|
||||
const info = RPC.getInfoObject(this.rpcConfig);
|
||||
|
||||
this.fnSetInfo(info);
|
||||
|
||||
return info.latestBlock;
|
||||
}
|
||||
|
||||
// This method will get the total balances
|
||||
async fetchTotalBalance() {
|
||||
const balanceStr = native.litelib_execute('balance', '');
|
||||
const balanceJSON = JSON.parse(balanceStr);
|
||||
|
||||
// Total Balance
|
||||
const balance = new TotalBalance();
|
||||
balance.private = balanceJSON.zbalance / 10 ** 8;
|
||||
balance.transparent = balanceJSON.tbalance / 10 ** 8;
|
||||
balance.verifiedPrivate = balanceJSON.verified_zbalance / 10 ** 8;
|
||||
balance.total = balance.private + balance.transparent;
|
||||
this.fnSetTotalBalance(balance);
|
||||
|
||||
// Fetch pending notes and UTXOs
|
||||
const pendingNotes = native.litelib_execute('notes', '');
|
||||
const pendingJSON = JSON.parse(pendingNotes);
|
||||
|
||||
const pendingAddressBalances = new Map();
|
||||
|
||||
// Process sapling notes
|
||||
pendingJSON.pending_notes.forEach(s => {
|
||||
pendingAddressBalances.set(s.address, s.value);
|
||||
});
|
||||
|
||||
// Process UTXOs
|
||||
pendingJSON.pending_utxos.forEach(s => {
|
||||
pendingAddressBalances.set(s.address, s.value);
|
||||
});
|
||||
|
||||
// Addresses with Balance. The lite client reports balances in zatoshi, so divide by 10^8;
|
||||
const zaddresses = balanceJSON.z_addresses
|
||||
.map(o => {
|
||||
// If this has any unconfirmed txns, show that in the UI
|
||||
const ab = new AddressBalance(o.address, o.zbalance / 10 ** 8);
|
||||
if (pendingAddressBalances.has(ab.address)) {
|
||||
ab.containsPending = true;
|
||||
}
|
||||
return ab;
|
||||
})
|
||||
.filter(ab => ab.balance > 0);
|
||||
|
||||
const taddresses = balanceJSON.t_addresses
|
||||
.map(o => {
|
||||
// If this has any unconfirmed txns, show that in the UI
|
||||
const ab = new AddressBalance(o.address, o.balance / 10 ** 8);
|
||||
if (pendingAddressBalances.has(ab.address)) {
|
||||
ab.containsPending = true;
|
||||
}
|
||||
return ab;
|
||||
})
|
||||
.filter(ab => ab.balance > 0);
|
||||
|
||||
const addresses = zaddresses.concat(taddresses);
|
||||
|
||||
this.fnSetAddressesWithBalance(addresses);
|
||||
|
||||
// Also set all addresses
|
||||
const allZAddresses = balanceJSON.z_addresses.map(o => o.address);
|
||||
const allTAddresses = balanceJSON.t_addresses.map(o => o.address);
|
||||
const allAddresses = allZAddresses.concat(allTAddresses);
|
||||
|
||||
this.fnSetAllAddresses(allAddresses);
|
||||
}
|
||||
|
||||
static getPrivKeyAsString(address: string): string {
|
||||
const privKeyStr = native.litelib_execute('export', address);
|
||||
const privKeyJSON = JSON.parse(privKeyStr);
|
||||
|
||||
return privKeyJSON[0].private_key;
|
||||
}
|
||||
|
||||
static createNewAddress(zaddress: boolean) {
|
||||
const addrStr = native.litelib_execute('new', zaddress ? 'z' : 't');
|
||||
const addrJSON = JSON.parse(addrStr);
|
||||
|
||||
return addrJSON[0];
|
||||
}
|
||||
|
||||
static fetchSeed(): string {
|
||||
const seedStr = native.litelib_execute('seed', '');
|
||||
const seedJSON = JSON.parse(seedStr);
|
||||
|
||||
return seedJSON.seed;
|
||||
}
|
||||
|
||||
// Fetch all T and Z transactions
|
||||
async fetchTandZTransactions(latestBlockHeight: number) {
|
||||
const listStr = native.litelib_execute('list', '');
|
||||
const listJSON = JSON.parse(listStr);
|
||||
|
||||
const txlist = listJSON.map(tx => {
|
||||
const transaction = new Transaction();
|
||||
|
||||
const type = tx.outgoing_metadata ? 'sent' : 'receive';
|
||||
|
||||
transaction.address =
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
type === 'sent' ? (tx.outgoing_metadata.length > 0 ? tx.outgoing_metadata[0].address : '') : tx.address;
|
||||
transaction.type = type;
|
||||
transaction.amount = tx.amount / 10 ** 8;
|
||||
transaction.confirmations = latestBlockHeight - tx.block_height + 1;
|
||||
transaction.txid = tx.txid;
|
||||
transaction.time = tx.datetime;
|
||||
if (tx.outgoing_metadata) {
|
||||
transaction.detailedTxns = tx.outgoing_metadata.map(o => {
|
||||
const detail = new TxDetail();
|
||||
detail.address = o.address;
|
||||
detail.amount = o.value / 10 ** 8;
|
||||
detail.memo = o.memo;
|
||||
|
||||
return detail;
|
||||
});
|
||||
} else {
|
||||
transaction.detailedTxns = [new TxDetail()];
|
||||
transaction.detailedTxns[0].address = tx.address;
|
||||
transaction.detailedTxns[0].amount = tx.amount / 10 ** 8;
|
||||
transaction.detailedTxns[0].memo = tx.memo;
|
||||
}
|
||||
|
||||
return transaction;
|
||||
});
|
||||
|
||||
txlist.sort((t1, t2) => t1.confirmations - t2.confirmations);
|
||||
|
||||
this.fnSetTransactionsList(txlist);
|
||||
}
|
||||
|
||||
// Send a transaction using the already constructed sendJson structure
|
||||
sendTransaction(sendJson: []): string {
|
||||
let sendStr;
|
||||
try {
|
||||
sendStr = native.litelib_execute('send', JSON.stringify(sendJson));
|
||||
} catch (err) {
|
||||
// TODO Show a modal with the error
|
||||
console.log(`Error sending Tx: ${err}`);
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (sendStr.startsWith('Error')) {
|
||||
// Throw the proper error
|
||||
throw sendStr.split(/[\r\n]+/)[0];
|
||||
}
|
||||
|
||||
console.log(`Send response: ${sendStr}`);
|
||||
const sendJSON = JSON.parse(sendStr);
|
||||
const { txid, error } = sendJSON;
|
||||
|
||||
if (error) {
|
||||
console.log(`Error sending Tx: ${error}`);
|
||||
throw error;
|
||||
} else {
|
||||
// And refresh data (full refresh)
|
||||
this.refresh(null);
|
||||
|
||||
return txid;
|
||||
}
|
||||
}
|
||||
|
||||
async encryptWallet(password): boolean {
|
||||
const resultStr = native.litelib_execute('encrypt', password);
|
||||
const resultJSON = JSON.parse(resultStr);
|
||||
|
||||
// To update the wallet encryption status
|
||||
this.fetchInfo();
|
||||
|
||||
// And save the wallet
|
||||
RPC.doSave();
|
||||
|
||||
return resultJSON.result === 'success';
|
||||
}
|
||||
|
||||
async decryptWallet(password): boolean {
|
||||
const resultStr = native.litelib_execute('decrypt', password);
|
||||
const resultJSON = JSON.parse(resultStr);
|
||||
|
||||
// To update the wallet encryption status
|
||||
this.fetchInfo();
|
||||
|
||||
// And save the wallet
|
||||
RPC.doSave();
|
||||
|
||||
return resultJSON.result === 'success';
|
||||
}
|
||||
|
||||
async lockWallet(): boolean {
|
||||
const resultStr = native.litelib_execute('lock', '');
|
||||
const resultJSON = JSON.parse(resultStr);
|
||||
|
||||
// To update the wallet encryption status
|
||||
this.fetchInfo();
|
||||
|
||||
return resultJSON.result === 'success';
|
||||
}
|
||||
|
||||
async unlockWallet(password: string): boolean {
|
||||
const resultStr = native.litelib_execute('unlock', password);
|
||||
const resultJSON = JSON.parse(resultStr);
|
||||
|
||||
// To update the wallet encryption status
|
||||
this.fetchInfo();
|
||||
|
||||
return resultJSON.result === 'success';
|
||||
}
|
||||
|
||||
setupNextZecPriceRefresh(retryCount: number, timeout: number) {
|
||||
// Every hour
|
||||
this.priceTimerID = setTimeout(() => this.getZecPrice(retryCount), timeout);
|
||||
}
|
||||
|
||||
async getZecPrice(retryCount: number) {
|
||||
if (!retryCount) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
retryCount = 0;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await new Promise((resolve, reject) => {
|
||||
axios('https://api.coinmarketcap.com/v1/ticker/', {
|
||||
method: 'GET'
|
||||
})
|
||||
.then(r => resolve(r.data))
|
||||
.catch(err => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
|
||||
const zecData = response.find(i => i.symbol.toUpperCase() === 'ZEC');
|
||||
if (zecData) {
|
||||
this.fnSetZecPrice(zecData.price_usd);
|
||||
this.setupNextZecPriceRefresh(0, 1000 * 60 * 60); // Every hour
|
||||
} else {
|
||||
this.fnSetZecPrice(null);
|
||||
let timeout = 1000 * 60; // 1 minute
|
||||
if (retryCount > 5) {
|
||||
timeout = 1000 * 60 * 60; // an hour later
|
||||
}
|
||||
this.setupNextZecPriceRefresh(retryCount + 1, timeout);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
this.fnSetZecPrice(null);
|
||||
let timeout = 1000 * 60; // 1 minute
|
||||
if (retryCount > 5) {
|
||||
timeout = 1000 * 60 * 60; // an hour later
|
||||
}
|
||||
this.setupNextZecPriceRefresh(retryCount + 1, timeout);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { remote } from 'electron';
|
||||
import { AddressBookEntry } from '../components/AppState';
|
||||
|
||||
// Utility class to save / read the address book.
|
||||
export default class AddressbookImpl {
|
||||
static async getFileName() {
|
||||
const dir = path.join(remote.app.getPath('appData'), 'zecwallet');
|
||||
if (!fs.existsSync(dir)) {
|
||||
await fs.promises.mkdir(dir);
|
||||
}
|
||||
const fileName = path.join(dir, 'AddressBook.json');
|
||||
|
||||
return fileName;
|
||||
}
|
||||
|
||||
// Write the address book to disk
|
||||
static async writeAddressBook(ab: AddressBookEntry[]) {
|
||||
const fileName = await this.getFileName();
|
||||
|
||||
await fs.promises.writeFile(fileName, JSON.stringify(ab));
|
||||
}
|
||||
|
||||
// Read the address book
|
||||
static async readAddressBook(): AddressBookEntry[] {
|
||||
const fileName = await this.getFileName();
|
||||
|
||||
try {
|
||||
return JSON.parse(await fs.promises.readFile(fileName));
|
||||
} catch (err) {
|
||||
// File probably doesn't exist, so return nothing
|
||||
console.log(err);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
import os from 'os';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { remote } from 'electron';
|
||||
import { Transaction, TxDetail } from '../components/AppState';
|
||||
|
||||
export default class SentTxStore {
|
||||
static locateSentTxStore() {
|
||||
if (os.platform() === 'darwin') {
|
||||
return path.join(remote.app.getPath('appData'), 'Zcash', 'senttxstore.dat');
|
||||
}
|
||||
|
||||
if (os.platform() === 'linux') {
|
||||
return path.join(
|
||||
remote.app.getPath('home'),
|
||||
'.local',
|
||||
'share',
|
||||
'zec-qt-wallet-org',
|
||||
'zec-qt-wallet',
|
||||
'senttxstore.dat'
|
||||
);
|
||||
}
|
||||
|
||||
return path.join(remote.app.getPath('appData'), 'Zcash', 'senttxstore.dat');
|
||||
}
|
||||
|
||||
static async loadSentTxns(): Transaction[] {
|
||||
try {
|
||||
const sentTx = JSON.parse(await fs.promises.readFile(SentTxStore.locateSentTxStore()));
|
||||
|
||||
return sentTx.map(s => {
|
||||
const transction = new Transaction();
|
||||
transction.type = s.type;
|
||||
transction.amount = s.amount;
|
||||
transction.address = s.from;
|
||||
transction.txid = s.txid;
|
||||
transction.time = s.datetime;
|
||||
transction.detailedTxns = [new TxDetail()];
|
||||
transction.detailedTxns[0].address = s.address;
|
||||
transction.detailedTxns[0].amount = s.amount;
|
||||
transction.detailedTxns[0].memo = s.memo;
|
||||
|
||||
return transction;
|
||||
});
|
||||
} catch (err) {
|
||||
// If error for whatever reason (most likely, file not found), just return an empty array
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
/* eslint-disable no-unused-vars */
|
||||
/* eslint-disable no-else-return */
|
||||
/* eslint-disable no-plusplus */
|
||||
export const NO_CONNECTION: string = 'Could not connect to zcashd';
|
||||
|
||||
export default class Utils {
|
||||
static isSapling(addr: string): boolean {
|
||||
if (!addr) return false;
|
||||
return new RegExp('^z[a-z0-9]{77}$').test(addr) || new RegExp('^ztestsapling[a-z0-9]{76}$').test(addr);
|
||||
}
|
||||
|
||||
static isSprout(addr: string): boolean {
|
||||
if (!addr) return false;
|
||||
return new RegExp('^z[a-zA-Z0-9]{94}$').test(addr);
|
||||
}
|
||||
|
||||
static isZaddr(addr: string): boolean {
|
||||
if (!addr) return false;
|
||||
return Utils.isSapling(addr) || Utils.isSprout(addr);
|
||||
}
|
||||
|
||||
static isTransparent(addr: string): boolean {
|
||||
if (!addr) return false;
|
||||
return new RegExp('^t[a-zA-Z0-9]{34}$').test(addr);
|
||||
}
|
||||
|
||||
// Convert to max 8 decimal places, and remove trailing zeros
|
||||
static maxPrecision(v: number): string {
|
||||
if (!v) return v;
|
||||
|
||||
if (typeof v === 'string' || v instanceof String) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
v = parseFloat(v);
|
||||
}
|
||||
|
||||
return v.toFixed(8);
|
||||
}
|
||||
|
||||
static splitZecAmountIntoBigSmall(zecValue: number) {
|
||||
if (!zecValue) {
|
||||
return { bigPart: zecValue, smallPart: '' };
|
||||
}
|
||||
|
||||
let bigPart = Utils.maxPrecision(zecValue);
|
||||
let smallPart = '';
|
||||
|
||||
if (bigPart.indexOf('.') >= 0) {
|
||||
const decimalPart = bigPart.substr(bigPart.indexOf('.') + 1);
|
||||
if (decimalPart.length > 4) {
|
||||
smallPart = decimalPart.substr(4);
|
||||
bigPart = bigPart.substr(0, bigPart.length - smallPart.length);
|
||||
|
||||
// Pad the small part with trailing 0s
|
||||
while (smallPart.length < 4) {
|
||||
smallPart += '0';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (smallPart === '0000') {
|
||||
smallPart = '';
|
||||
}
|
||||
|
||||
return { bigPart, smallPart };
|
||||
}
|
||||
|
||||
static splitStringIntoChunks(s: string, numChunks: number) {
|
||||
if (numChunks > s.length) return [s];
|
||||
if (s.length < 16) return [s];
|
||||
|
||||
const chunkSize = Math.round(s.length / numChunks);
|
||||
const chunks = [];
|
||||
for (let i = 0; i < numChunks - 1; i++) {
|
||||
chunks.push(s.substr(i * chunkSize, chunkSize));
|
||||
}
|
||||
// Last chunk might contain un-even length
|
||||
chunks.push(s.substr((numChunks - 1) * chunkSize));
|
||||
|
||||
return chunks;
|
||||
}
|
||||
|
||||
static nextToAddrID: number = 0;
|
||||
|
||||
static getNextToAddrID(): number {
|
||||
// eslint-disable-next-line no-plusplus
|
||||
return Utils.nextToAddrID++;
|
||||
}
|
||||
|
||||
static getDonationAddress(testnet: boolean): string {
|
||||
if (testnet) {
|
||||
return 'ztestsapling1wn6889vznyu42wzmkakl2effhllhpe4azhu696edg2x6me4kfsnmqwpglaxzs7tmqsq7kudemp5';
|
||||
} else {
|
||||
return 'zs1gv64eu0v2wx7raxqxlmj354y9ycznwaau9kduljzczxztvs4qcl00kn2sjxtejvrxnkucw5xx9u';
|
||||
}
|
||||
}
|
||||
|
||||
static getDefaultDonationAmount(testnet: boolean): number {
|
||||
return 0.1;
|
||||
}
|
||||
|
||||
static getDefaultDonationMemo(testnet: boolean): string {
|
||||
return 'Thanks for supporting Zecwallet!';
|
||||
}
|
||||
|
||||
static getZecToUsdString(price: number | null, zecValue: number | null): string {
|
||||
if (!price || !zecValue) {
|
||||
return 'USD --';
|
||||
}
|
||||
|
||||
return `USD ${(price * zecValue).toFixed(2)}`;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
<RCC>
|
||||
<qresource prefix="/fonts">
|
||||
<file>res/Ubuntu-R.ttf</file>
|
||||
</qresource>
|
||||
<qresource prefix="/icons">
|
||||
<file>res/connected.gif</file>
|
||||
<file>res/loading.gif</file>
|
||||
<file>res/paymentreq.gif</file>
|
||||
<file>res/icon.ico</file>
|
||||
</qresource>
|
||||
<qresource prefix="/img">
|
||||
<file>res/zcashdlogo.gif</file>
|
||||
<file>res/logobig.gif</file>
|
||||
</qresource>
|
||||
<qresource prefix="/translations">
|
||||
<file>res/zec_qt_wallet_es.qm</file>
|
||||
<file>res/zec_qt_wallet_fr.qm</file>
|
||||
<file>res/zec_qt_wallet_pt.qm</file>
|
||||
<file>res/zec_qt_wallet_it.qm</file>
|
||||
<file>res/zec_qt_wallet_zh.qm</file>
|
||||
<file>res/zec_qt_wallet_tr.qm</file>
|
||||
</qresource>
|
||||
<qresource prefix="/css">
|
||||
<file>res/css/blue.css</file>
|
||||
<file>res/css/dark.css</file>
|
||||
<file>res/css/default.css</file>
|
||||
<file>res/css/light.css</file>
|
||||
</qresource>
|
||||
<qresource prefix="/images/blue">
|
||||
<file>res/images/blue/unchecked.png</file>
|
||||
<file>res/images/blue/checked.png</file>
|
||||
<file>res/images/blue/blue_downArrow.png</file>
|
||||
<file>res/images/blue/blue_downArrow_small.png</file>
|
||||
<file>res/images/blue/blue_upArrow_small.png</file>
|
||||
<file>res/images/blue/blue_leftArrow_small.png</file>
|
||||
<file>res/images/blue/blue_rightArrow_small.png</file>
|
||||
<file>res/images/blue/blue_qtreeview_selected.png</file>
|
||||
</qresource>
|
||||
</RCC>
|
|
@ -0,0 +1,39 @@
|
|||
image: Visual Studio 2017
|
||||
|
||||
platform:
|
||||
- x64
|
||||
|
||||
environment:
|
||||
matrix:
|
||||
- nodejs_version: 10
|
||||
|
||||
cache:
|
||||
- '%LOCALAPPDATA%/Yarn'
|
||||
- node_modules
|
||||
- app/node_modules
|
||||
- flow-typed
|
||||
- '%USERPROFILE%\.electron'
|
||||
|
||||
matrix:
|
||||
fast_finish: true
|
||||
|
||||
build: off
|
||||
|
||||
version: '{build}'
|
||||
|
||||
shallow_clone: true
|
||||
|
||||
clone_depth: 1
|
||||
|
||||
install:
|
||||
- ps: Install-Product node $env:nodejs_version x64
|
||||
- set CI=true
|
||||
- yarn
|
||||
|
||||
test_script:
|
||||
- yarn package-ci
|
||||
- yarn lint
|
||||
# - yarn flow
|
||||
- yarn test
|
||||
- yarn build-e2e
|
||||
- yarn test-e2e
|
|
@ -0,0 +1,66 @@
|
|||
/* eslint global-require: off */
|
||||
|
||||
const developmentEnvironments = ['development', 'test'];
|
||||
|
||||
const developmentPlugins = [require('react-hot-loader/babel')];
|
||||
|
||||
const productionPlugins = [
|
||||
require('babel-plugin-dev-expression'),
|
||||
|
||||
// babel-preset-react-optimize
|
||||
require('@babel/plugin-transform-react-constant-elements'),
|
||||
require('@babel/plugin-transform-react-inline-elements'),
|
||||
require('babel-plugin-transform-react-remove-prop-types')
|
||||
];
|
||||
|
||||
module.exports = api => {
|
||||
// see docs about api at https://babeljs.io/docs/en/config-files#apicache
|
||||
|
||||
const development = api.env(developmentEnvironments);
|
||||
|
||||
return {
|
||||
presets: [
|
||||
[
|
||||
require('@babel/preset-env'),
|
||||
{
|
||||
targets: { electron: require('electron/package.json').version }
|
||||
}
|
||||
],
|
||||
require('@babel/preset-flow'),
|
||||
[require('@babel/preset-react'), { development }]
|
||||
],
|
||||
plugins: [
|
||||
// Stage 0
|
||||
require('@babel/plugin-proposal-function-bind'),
|
||||
|
||||
// Stage 1
|
||||
require('@babel/plugin-proposal-export-default-from'),
|
||||
require('@babel/plugin-proposal-logical-assignment-operators'),
|
||||
[require('@babel/plugin-proposal-optional-chaining'), { loose: false }],
|
||||
[
|
||||
require('@babel/plugin-proposal-pipeline-operator'),
|
||||
{ proposal: 'minimal' }
|
||||
],
|
||||
[
|
||||
require('@babel/plugin-proposal-nullish-coalescing-operator'),
|
||||
{ loose: false }
|
||||
],
|
||||
require('@babel/plugin-proposal-do-expressions'),
|
||||
|
||||
// Stage 2
|
||||
[require('@babel/plugin-proposal-decorators'), { legacy: true }],
|
||||
require('@babel/plugin-proposal-function-sent'),
|
||||
require('@babel/plugin-proposal-export-namespace-from'),
|
||||
require('@babel/plugin-proposal-numeric-separator'),
|
||||
require('@babel/plugin-proposal-throw-expressions'),
|
||||
|
||||
// Stage 3
|
||||
require('@babel/plugin-syntax-dynamic-import'),
|
||||
require('@babel/plugin-syntax-import-meta'),
|
||||
[require('@babel/plugin-proposal-class-properties'), { loose: true }],
|
||||
require('@babel/plugin-proposal-json-strings'),
|
||||
|
||||
...(development ? developmentPlugins : productionPlugins)
|
||||
]
|
||||
};
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
echo "::set-env name=VERSION::1.1.0-beta1"
|
|
@ -0,0 +1,4 @@
|
|||
#!/bin/bash
|
||||
VERSION="1.1.0-beta1"
|
||||
|
||||
echo "::set-env name=VERSION::$VERSION"
|
|
@ -0,0 +1,47 @@
|
|||
#!/bin/bash
|
||||
# Accept the variables as command line arguments as well
|
||||
POSITIONAL=()
|
||||
while [[ $# -gt 0 ]]
|
||||
do
|
||||
key="$1"
|
||||
|
||||
case $key in
|
||||
-v|--version)
|
||||
APP_VERSION="$2"
|
||||
shift # past argument
|
||||
shift # past value
|
||||
;;
|
||||
*) # unknown option
|
||||
POSITIONAL+=("$1") # save it in an array for later
|
||||
shift # past argument
|
||||
;;
|
||||
esac
|
||||
done
|
||||
set -- "${POSITIONAL[@]}" # restore positional parameters
|
||||
|
||||
if [ -z $APP_VERSION ]; then echo "APP_VERSION is not set"; exit 1; fi
|
||||
|
||||
# Store the hash and signatures here
|
||||
cd release
|
||||
rm -rf signatures
|
||||
mkdir signatures
|
||||
|
||||
# Remove previous signatures/hashes
|
||||
rm -f sha256sum-$APP_VERSION.txt
|
||||
rm -f signatures-$APP_VERSION.zip
|
||||
|
||||
# sha256sum the binaries
|
||||
sha256sum Zecwallet*$APP_VERSION* > sha256sum-$APP_VERSION.txt
|
||||
|
||||
OIFS="$IFS"
|
||||
IFS=$'\n'
|
||||
|
||||
for i in `find ./ -iname "Zecwallet*$APP_VERSION*" -o -iname "sha256sum-$APP_VERSION.txt"`; do
|
||||
echo "Signing" "$i"
|
||||
gpg --batch --output "signatures/$i.sig" --detach-sig "$i"
|
||||
done
|
||||
|
||||
cp sha256sum-$APP_VERSION.txt signatures/
|
||||
cp ../configs/SIGNATURES_README signatures/
|
||||
|
||||
zip -r signatures-$APP_VERSION.zip signatures/
|
|
@ -4,10 +4,9 @@ Verify the hashes by running:
|
|||
sha256sum -c sha256sum-vX.Y.Z.txt
|
||||
|
||||
Verify signatures:
|
||||
1. First, import the public key (Available on GitHub
|
||||
1. First, import the public key (Available on GitHub
|
||||
at https://github.com/ZcashFoundation/zecwallet/blob/master/public_key.asc)
|
||||
gpg --import public_key.asc
|
||||
|
||||
2. Verify signature
|
||||
2. Verify signature
|
||||
gpg --verify <filename.sig> <downloaded-filename-to-verify>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<?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.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
|
@ -0,0 +1,51 @@
|
|||
/**
|
||||
* Base webpack config used across other specific configs
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import webpack from 'webpack';
|
||||
import { dependencies as externals } from '../app/package.json';
|
||||
|
||||
export default {
|
||||
externals: [...Object.keys(externals || {}), 'bufferutil', 'utf-8-validate'],
|
||||
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.jsx?$/,
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
cacheDirectory: true
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
output: {
|
||||
path: path.join(__dirname, '..', 'app'),
|
||||
// https://github.com/webpack/webpack/issues/1114
|
||||
libraryTarget: 'commonjs2'
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine the array of extensions that should be used to resolve modules.
|
||||
*/
|
||||
resolve: {
|
||||
extensions: ['.js', '.jsx', '.json'],
|
||||
modules: [path.join(__dirname, '..', 'app'), 'node_modules'],
|
||||
alias: {
|
||||
ws: path.resolve(path.join(__dirname, '..', 'node_modules/ws/index.js'))
|
||||
}
|
||||
},
|
||||
|
||||
plugins: [
|
||||
new webpack.EnvironmentPlugin({
|
||||
NODE_ENV: 'production'
|
||||
}),
|
||||
|
||||
new webpack.NamedModulesPlugin()
|
||||
]
|
||||
};
|
|
@ -0,0 +1,4 @@
|
|||
/* eslint import/no-unresolved: off, import/no-self-import: off */
|
||||
require('@babel/register');
|
||||
|
||||
module.exports = require('./webpack.config.renderer.dev.babel').default;
|
|
@ -0,0 +1,72 @@
|
|||
/**
|
||||
* Webpack config for production electron main process
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import webpack from 'webpack';
|
||||
import merge from 'webpack-merge';
|
||||
import TerserPlugin from 'terser-webpack-plugin';
|
||||
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
|
||||
import baseConfig from './webpack.config.base';
|
||||
import CheckNodeEnv from '../internals/scripts/CheckNodeEnv';
|
||||
|
||||
CheckNodeEnv('production');
|
||||
|
||||
export default merge.smart(baseConfig, {
|
||||
devtool: 'source-map',
|
||||
|
||||
mode: 'production',
|
||||
|
||||
target: 'electron-main',
|
||||
|
||||
entry: './app/main.dev',
|
||||
|
||||
output: {
|
||||
path: path.join(__dirname, '..'),
|
||||
filename: './app/main.prod.js'
|
||||
},
|
||||
|
||||
optimization: {
|
||||
minimizer: process.env.E2E_BUILD
|
||||
? []
|
||||
: [
|
||||
new TerserPlugin({
|
||||
parallel: true,
|
||||
sourceMap: true,
|
||||
cache: true
|
||||
})
|
||||
]
|
||||
},
|
||||
|
||||
plugins: [
|
||||
new BundleAnalyzerPlugin({
|
||||
analyzerMode: process.env.OPEN_ANALYZER === 'true' ? 'server' : 'disabled',
|
||||
openAnalyzer: process.env.OPEN_ANALYZER === 'true'
|
||||
}),
|
||||
|
||||
/**
|
||||
* Create global constants which can be configured at compile time.
|
||||
*
|
||||
* Useful for allowing different behaviour between development builds and
|
||||
* release builds
|
||||
*
|
||||
* NODE_ENV should be production so that modules do not perform certain
|
||||
* development checks
|
||||
*/
|
||||
new webpack.EnvironmentPlugin({
|
||||
NODE_ENV: 'production',
|
||||
DEBUG_PROD: false,
|
||||
START_MINIMIZED: false
|
||||
})
|
||||
],
|
||||
|
||||
/**
|
||||
* Disables webpack processing of __dirname and __filename.
|
||||
* If you run the bundle in node.js it falls back to these values of node.js.
|
||||
* https://github.com/webpack/webpack/issues/2010
|
||||
*/
|
||||
node: {
|
||||
__dirname: false,
|
||||
__filename: false
|
||||
}
|
||||
});
|
|
@ -0,0 +1,291 @@
|
|||
/* eslint global-require: off, import/no-dynamic-require: off */
|
||||
|
||||
/**
|
||||
* Build config for development electron renderer process that uses
|
||||
* Hot-Module-Replacement
|
||||
*
|
||||
* https://webpack.js.org/concepts/hot-module-replacement/
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import webpack from 'webpack';
|
||||
import chalk from 'chalk';
|
||||
import merge from 'webpack-merge';
|
||||
import { spawn, execSync } from 'child_process';
|
||||
import baseConfig from './webpack.config.base';
|
||||
import CheckNodeEnv from '../internals/scripts/CheckNodeEnv';
|
||||
|
||||
// When an ESLint server is running, we can't set the NODE_ENV so we'll check if it's
|
||||
// at the dev webpack config is not accidentally run in a production environment
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
CheckNodeEnv('development');
|
||||
}
|
||||
|
||||
const port = process.env.PORT || 1212;
|
||||
const publicPath = `http://localhost:${port}/dist`;
|
||||
const dll = path.join(__dirname, '..', 'dll');
|
||||
const manifest = path.resolve(dll, 'renderer.json');
|
||||
const requiredByDLLConfig = module.parent.filename.includes('webpack.config.renderer.dev.dll');
|
||||
|
||||
/**
|
||||
* Warn if the DLL is not built
|
||||
*/
|
||||
if (!requiredByDLLConfig && !(fs.existsSync(dll) && fs.existsSync(manifest))) {
|
||||
console.log(
|
||||
chalk.black.bgYellow.bold('The DLL files are missing. Sit back while we build them for you with "yarn build-dll"')
|
||||
);
|
||||
execSync('yarn build-dll');
|
||||
}
|
||||
|
||||
export default merge.smart(baseConfig, {
|
||||
devtool: 'inline-source-map',
|
||||
|
||||
mode: 'development',
|
||||
|
||||
target: 'electron-renderer',
|
||||
|
||||
entry: [
|
||||
...(process.env.PLAIN_HMR ? [] : ['react-hot-loader/patch']),
|
||||
`webpack-dev-server/client?http://localhost:${port}/`,
|
||||
'webpack/hot/only-dev-server',
|
||||
require.resolve('../app/index')
|
||||
],
|
||||
|
||||
output: {
|
||||
publicPath: `http://localhost:${port}/dist/`,
|
||||
filename: 'renderer.dev.js'
|
||||
},
|
||||
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.jsx?$/,
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
cacheDirectory: true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.global\.css$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'style-loader'
|
||||
},
|
||||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
sourceMap: true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /^((?!\.global).)*\.css$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'style-loader'
|
||||
},
|
||||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
modules: {
|
||||
localIdentName: '[name]__[local]__[hash:base64:5]'
|
||||
},
|
||||
sourceMap: true,
|
||||
importLoaders: 1
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
// SASS support - compile all .global.scss files and pipe it to style.css
|
||||
{
|
||||
test: /\.global\.(scss|sass)$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'style-loader'
|
||||
},
|
||||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
sourceMap: true
|
||||
}
|
||||
},
|
||||
{
|
||||
loader: 'sass-loader'
|
||||
}
|
||||
]
|
||||
},
|
||||
// SASS support - compile all other .scss files and pipe it to style.css
|
||||
{
|
||||
test: /^((?!\.global).)*\.(scss|sass)$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'style-loader'
|
||||
},
|
||||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
modules: {
|
||||
localIdentName: '[name]__[local]__[hash:base64:5]'
|
||||
},
|
||||
sourceMap: true,
|
||||
importLoaders: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
loader: 'sass-loader'
|
||||
}
|
||||
]
|
||||
},
|
||||
// WOFF Font
|
||||
{
|
||||
test: /\.woff(\?v=\d+\.\d+\.\d+)?$/,
|
||||
use: {
|
||||
loader: 'url-loader',
|
||||
options: {
|
||||
limit: 10000,
|
||||
mimetype: 'application/font-woff'
|
||||
}
|
||||
}
|
||||
},
|
||||
// WOFF2 Font
|
||||
{
|
||||
test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/,
|
||||
use: {
|
||||
loader: 'url-loader',
|
||||
options: {
|
||||
limit: 10000,
|
||||
mimetype: 'application/font-woff'
|
||||
}
|
||||
}
|
||||
},
|
||||
// TTF Font
|
||||
{
|
||||
test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/,
|
||||
use: {
|
||||
loader: 'url-loader',
|
||||
options: {
|
||||
limit: 10000,
|
||||
mimetype: 'application/octet-stream'
|
||||
}
|
||||
}
|
||||
},
|
||||
// EOT Font
|
||||
{
|
||||
test: /\.eot(\?v=\d+\.\d+\.\d+)?$/,
|
||||
use: 'file-loader'
|
||||
},
|
||||
// SVG Font
|
||||
{
|
||||
test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
|
||||
use: {
|
||||
loader: 'url-loader',
|
||||
options: {
|
||||
limit: 10000,
|
||||
mimetype: 'image/svg+xml'
|
||||
}
|
||||
}
|
||||
},
|
||||
// Common Image Formats
|
||||
{
|
||||
test: /\.(?:ico|gif|png|jpg|jpeg|webp)$/,
|
||||
use: 'url-loader'
|
||||
},
|
||||
{
|
||||
test: /\.node$/,
|
||||
use: [
|
||||
{
|
||||
loader: path.resolve('./internals/scripts/NativeLoader.js'),
|
||||
options: {
|
||||
name: '[name]-[hash].[ext]'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'react-dom': '@hot-loader/react-dom',
|
||||
ws: path.resolve(path.join(__dirname, '..', 'node_modules/ws/index.js'))
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
requiredByDLLConfig
|
||||
? null
|
||||
: new webpack.DllReferencePlugin({
|
||||
context: path.join(__dirname, '..', 'dll'),
|
||||
manifest: require(manifest),
|
||||
sourceType: 'var'
|
||||
}),
|
||||
|
||||
new webpack.HotModuleReplacementPlugin({
|
||||
multiStep: true
|
||||
}),
|
||||
|
||||
new webpack.NoEmitOnErrorsPlugin(),
|
||||
|
||||
/**
|
||||
* Create global constants which can be configured at compile time.
|
||||
*
|
||||
* Useful for allowing different behaviour between development builds and
|
||||
* release builds
|
||||
*
|
||||
* NODE_ENV should be production so that modules do not perform certain
|
||||
* development checks
|
||||
*
|
||||
* By default, use 'development' as NODE_ENV. This can be overridden with
|
||||
* 'staging', for example, by changing the ENV variables in the npm scripts
|
||||
*/
|
||||
new webpack.EnvironmentPlugin({
|
||||
NODE_ENV: 'development'
|
||||
}),
|
||||
|
||||
new webpack.LoaderOptionsPlugin({
|
||||
debug: true
|
||||
})
|
||||
],
|
||||
|
||||
node: {
|
||||
__dirname: false,
|
||||
__filename: false
|
||||
},
|
||||
|
||||
devServer: {
|
||||
port,
|
||||
publicPath,
|
||||
compress: true,
|
||||
noInfo: true,
|
||||
stats: 'errors-only',
|
||||
inline: true,
|
||||
lazy: false,
|
||||
hot: true,
|
||||
headers: { 'Access-Control-Allow-Origin': '*' },
|
||||
contentBase: path.join(__dirname, 'dist'),
|
||||
watchOptions: {
|
||||
aggregateTimeout: 300,
|
||||
ignored: /node_modules/,
|
||||
poll: 100
|
||||
},
|
||||
historyApiFallback: {
|
||||
verbose: true,
|
||||
disableDotRule: false
|
||||
},
|
||||
before() {
|
||||
if (process.env.START_HOT) {
|
||||
console.log('Starting Main Process...');
|
||||
spawn('npm', ['run', 'start-main-dev'], {
|
||||
shell: true,
|
||||
env: process.env,
|
||||
stdio: 'inherit'
|
||||
})
|
||||
.on('close', code => process.exit(code))
|
||||
.on('error', spawnError => console.error(spawnError));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
|
@ -0,0 +1,74 @@
|
|||
/* eslint global-require: off, import/no-dynamic-require: off */
|
||||
|
||||
/**
|
||||
* Builds the DLL for development electron renderer process
|
||||
*/
|
||||
|
||||
import webpack from 'webpack';
|
||||
import path from 'path';
|
||||
import merge from 'webpack-merge';
|
||||
import baseConfig from './webpack.config.base';
|
||||
import { dependencies } from '../package.json';
|
||||
import CheckNodeEnv from '../internals/scripts/CheckNodeEnv';
|
||||
|
||||
CheckNodeEnv('development');
|
||||
|
||||
const dist = path.join(__dirname, '..', 'dll');
|
||||
|
||||
export default merge.smart(baseConfig, {
|
||||
context: path.join(__dirname, '..'),
|
||||
|
||||
devtool: 'eval',
|
||||
|
||||
mode: 'development',
|
||||
|
||||
target: 'electron-renderer',
|
||||
|
||||
externals: ['fsevents', 'crypto-browserify'],
|
||||
|
||||
/**
|
||||
* Use `module` from `webpack.config.renderer.dev.js`
|
||||
*/
|
||||
module: require('./webpack.config.renderer.dev.babel').default.module,
|
||||
|
||||
entry: {
|
||||
renderer: Object.keys(dependencies || {})
|
||||
},
|
||||
|
||||
output: {
|
||||
library: 'renderer',
|
||||
path: dist,
|
||||
filename: '[name].dev.dll.js',
|
||||
libraryTarget: 'var'
|
||||
},
|
||||
|
||||
plugins: [
|
||||
new webpack.DllPlugin({
|
||||
path: path.join(dist, '[name].json'),
|
||||
name: '[name]'
|
||||
}),
|
||||
|
||||
/**
|
||||
* Create global constants which can be configured at compile time.
|
||||
*
|
||||
* Useful for allowing different behaviour between development builds and
|
||||
* release builds
|
||||
*
|
||||
* NODE_ENV should be production so that modules do not perform certain
|
||||
* development checks
|
||||
*/
|
||||
new webpack.EnvironmentPlugin({
|
||||
NODE_ENV: 'development'
|
||||
}),
|
||||
|
||||
new webpack.LoaderOptionsPlugin({
|
||||
debug: true,
|
||||
options: {
|
||||
context: path.join(__dirname, '..', 'app'),
|
||||
output: {
|
||||
path: path.join(__dirname, '..', 'dll')
|
||||
}
|
||||
}
|
||||
})
|
||||
]
|
||||
});
|
|
@ -0,0 +1,228 @@
|
|||
/**
|
||||
* Build config for electron renderer process
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import webpack from 'webpack';
|
||||
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
|
||||
import OptimizeCSSAssetsPlugin from 'optimize-css-assets-webpack-plugin';
|
||||
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
|
||||
import merge from 'webpack-merge';
|
||||
import TerserPlugin from 'terser-webpack-plugin';
|
||||
import baseConfig from './webpack.config.base';
|
||||
import CheckNodeEnv from '../internals/scripts/CheckNodeEnv';
|
||||
|
||||
CheckNodeEnv('production');
|
||||
|
||||
export default merge.smart(baseConfig, {
|
||||
devtool: 'source-map',
|
||||
|
||||
mode: 'production',
|
||||
|
||||
target: 'electron-renderer',
|
||||
|
||||
entry: path.join(__dirname, '..', 'app/index'),
|
||||
|
||||
output: {
|
||||
path: path.join(__dirname, '..', 'app/dist'),
|
||||
publicPath: './dist/',
|
||||
filename: 'renderer.prod.js'
|
||||
},
|
||||
|
||||
module: {
|
||||
rules: [
|
||||
// Extract all .global.css to style.css as is
|
||||
{
|
||||
test: /\.global\.css$/,
|
||||
use: [
|
||||
{
|
||||
loader: MiniCssExtractPlugin.loader,
|
||||
options: {
|
||||
publicPath: './'
|
||||
}
|
||||
},
|
||||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
sourceMap: true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
// Pipe other styles through css modules and append to style.css
|
||||
{
|
||||
test: /^((?!\.global).)*\.css$/,
|
||||
use: [
|
||||
{
|
||||
loader: MiniCssExtractPlugin.loader
|
||||
},
|
||||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
modules: {
|
||||
localIdentName: '[name]__[local]__[hash:base64:5]'
|
||||
},
|
||||
sourceMap: true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
// Add SASS support - compile all .global.scss files and pipe it to style.css
|
||||
{
|
||||
test: /\.global\.(scss|sass)$/,
|
||||
use: [
|
||||
{
|
||||
loader: MiniCssExtractPlugin.loader
|
||||
},
|
||||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
sourceMap: true,
|
||||
importLoaders: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
loader: 'sass-loader',
|
||||
options: {
|
||||
sourceMap: true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
// Add SASS support - compile all other .scss files and pipe it to style.css
|
||||
{
|
||||
test: /^((?!\.global).)*\.(scss|sass)$/,
|
||||
use: [
|
||||
{
|
||||
loader: MiniCssExtractPlugin.loader
|
||||
},
|
||||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
modules: {
|
||||
localIdentName: '[name]__[local]__[hash:base64:5]'
|
||||
},
|
||||
importLoaders: 1,
|
||||
sourceMap: true
|
||||
}
|
||||
},
|
||||
{
|
||||
loader: 'sass-loader',
|
||||
options: {
|
||||
sourceMap: true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
// WOFF Font
|
||||
{
|
||||
test: /\.woff(\?v=\d+\.\d+\.\d+)?$/,
|
||||
use: {
|
||||
loader: 'url-loader',
|
||||
options: {
|
||||
limit: 10000,
|
||||
mimetype: 'application/font-woff'
|
||||
}
|
||||
}
|
||||
},
|
||||
// WOFF2 Font
|
||||
{
|
||||
test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/,
|
||||
use: {
|
||||
loader: 'url-loader',
|
||||
options: {
|
||||
limit: 10000,
|
||||
mimetype: 'application/font-woff'
|
||||
}
|
||||
}
|
||||
},
|
||||
// TTF Font
|
||||
{
|
||||
test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/,
|
||||
use: {
|
||||
loader: 'url-loader',
|
||||
options: {
|
||||
limit: 10000,
|
||||
mimetype: 'application/octet-stream'
|
||||
}
|
||||
}
|
||||
},
|
||||
// EOT Font
|
||||
{
|
||||
test: /\.eot(\?v=\d+\.\d+\.\d+)?$/,
|
||||
use: 'file-loader'
|
||||
},
|
||||
// SVG Font
|
||||
{
|
||||
test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
|
||||
use: {
|
||||
loader: 'url-loader',
|
||||
options: {
|
||||
limit: 10000,
|
||||
mimetype: 'image/svg+xml'
|
||||
}
|
||||
}
|
||||
},
|
||||
// Common Image Formats
|
||||
{
|
||||
test: /\.(?:ico|gif|png|jpg|jpeg|webp)$/,
|
||||
use: 'url-loader'
|
||||
},
|
||||
{
|
||||
test: /\.node$/,
|
||||
use: [
|
||||
{
|
||||
loader: path.resolve('./internals/scripts/NativeLoader.js'),
|
||||
options: {
|
||||
name: '[name]-[hash].[ext]'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
optimization: {
|
||||
minimizer: process.env.E2E_BUILD
|
||||
? []
|
||||
: [
|
||||
new TerserPlugin({
|
||||
parallel: true,
|
||||
sourceMap: true,
|
||||
cache: true
|
||||
}),
|
||||
new OptimizeCSSAssetsPlugin({
|
||||
cssProcessorOptions: {
|
||||
map: {
|
||||
inline: false,
|
||||
annotation: true
|
||||
}
|
||||
}
|
||||
})
|
||||
]
|
||||
},
|
||||
|
||||
plugins: [
|
||||
/**
|
||||
* Create global constants which can be configured at compile time.
|
||||
*
|
||||
* Useful for allowing different behaviour between development builds and
|
||||
* release builds
|
||||
*
|
||||
* NODE_ENV should be production so that modules do not perform certain
|
||||
* development checks
|
||||
*/
|
||||
new webpack.EnvironmentPlugin({
|
||||
NODE_ENV: 'production'
|
||||
}),
|
||||
|
||||
new MiniCssExtractPlugin({
|
||||
filename: 'style.css'
|
||||
}),
|
||||
|
||||
new BundleAnalyzerPlugin({
|
||||
analyzerMode: process.env.OPEN_ANALYZER === 'true' ? 'server' : 'disabled',
|
||||
openAnalyzer: process.env.OPEN_ANALYZER === 'true'
|
||||
})
|
||||
]
|
||||
});
|
|
@ -1,6 +0,0 @@
|
|||
<html>
|
||||
<head></head>
|
||||
<body>
|
||||
Hello World
|
||||
</body>
|
||||
</html>
|
Before Width: | Height: | Size: 178 KiB |
Before Width: | Height: | Size: 62 KiB |
Before Width: | Height: | Size: 93 KiB |
Before Width: | Height: | Size: 65 KiB |
Before Width: | Height: | Size: 138 KiB |
|
@ -0,0 +1,3 @@
|
|||
declare module 'module' {
|
||||
declare module.exports: any;
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
// @flow
|
||||
|
||||
declare export default { [key: string]: string }
|
|
@ -0,0 +1,2 @@
|
|||
// @flow
|
||||
declare export default string
|
|
@ -0,0 +1,5 @@
|
|||
const path = require('path');
|
||||
|
||||
require('@babel/register')({
|
||||
cwd: path.join(__dirname, '..', '..')
|
||||
});
|
|
@ -0,0 +1,35 @@
|
|||
// @flow
|
||||
// Check if the renderer and main bundles are built
|
||||
import path from 'path';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs';
|
||||
|
||||
function CheckBuildsExist() {
|
||||
const mainPath = path.join(__dirname, '..', '..', 'app', 'main.prod.js');
|
||||
const rendererPath = path.join(
|
||||
__dirname,
|
||||
'..',
|
||||
'..',
|
||||
'app',
|
||||
'dist',
|
||||
'renderer.prod.js'
|
||||
);
|
||||
|
||||
if (!fs.existsSync(mainPath)) {
|
||||
throw new Error(
|
||||
chalk.whiteBright.bgRed.bold(
|
||||
'The main process is not built yet. Build it by running "yarn build-main"'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(rendererPath)) {
|
||||
throw new Error(
|
||||
chalk.whiteBright.bgRed.bold(
|
||||
'The renderer process is not built yet. Build it by running "yarn build-renderer"'
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CheckBuildsExist();
|
|
@ -0,0 +1,51 @@
|
|||
// @flow
|
||||
import fs from 'fs';
|
||||
import chalk from 'chalk';
|
||||
import { execSync } from 'child_process';
|
||||
import { dependencies } from '../../package.json';
|
||||
|
||||
(() => {
|
||||
if (!dependencies) return;
|
||||
const dependenciesKeys = Object.keys(dependencies);
|
||||
const nativeDeps = fs
|
||||
.readdirSync('node_modules')
|
||||
.filter(folder => fs.existsSync(`node_modules/${folder}/binding.gyp`));
|
||||
try {
|
||||
// Find the reason for why the dependency is installed. If it is installed
|
||||
// because of a devDependency then that is okay. Warn when it is installed
|
||||
// because of a dependency
|
||||
const { dependencies: dependenciesObject } = JSON.parse(
|
||||
execSync(`npm ls ${nativeDeps.join(' ')} --json`).toString()
|
||||
);
|
||||
const rootDependencies = Object.keys(dependenciesObject);
|
||||
const filteredRootDependencies = rootDependencies.filter(rootDependency =>
|
||||
dependenciesKeys.includes(rootDependency)
|
||||
);
|
||||
if (filteredRootDependencies.length > 0) {
|
||||
const plural = filteredRootDependencies.length > 1;
|
||||
console.log(`
|
||||
${chalk.whiteBright.bgYellow.bold(
|
||||
'Webpack does not work with native dependencies.'
|
||||
)}
|
||||
${chalk.bold(filteredRootDependencies.join(', '))} ${
|
||||
plural ? 'are native dependencies' : 'is a native dependency'
|
||||
} and should be installed inside of the "./app" folder.
|
||||
First uninstall the packages from "./package.json":
|
||||
${chalk.whiteBright.bgGreen.bold('yarn remove your-package')}
|
||||
${chalk.bold(
|
||||
'Then, instead of installing the package to the root "./package.json":'
|
||||
)}
|
||||
${chalk.whiteBright.bgRed.bold('yarn add your-package')}
|
||||
${chalk.bold('Install the package to "./app/package.json"')}
|
||||
${chalk.whiteBright.bgGreen.bold('cd ./app && yarn add your-package')}
|
||||
Read more about native dependencies at:
|
||||
${chalk.bold(
|
||||
'https://github.com/electron-react-boilerplate/electron-react-boilerplate/wiki/Module-Structure----Two-package.json-Structure'
|
||||
)}
|
||||
`);
|
||||
process.exit(1);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Native dependencies could not be checked');
|
||||
}
|
||||
})();
|
|
@ -0,0 +1,17 @@
|
|||
// @flow
|
||||
import chalk from 'chalk';
|
||||
|
||||
export default function CheckNodeEnv(expectedEnv: string) {
|
||||
if (!expectedEnv) {
|
||||
throw new Error('"expectedEnv" not set');
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV !== expectedEnv) {
|
||||
console.log(
|
||||
chalk.whiteBright.bgRed.bold(
|
||||
`"process.env.NODE_ENV" must be "${expectedEnv}" to use this webpack config`
|
||||
)
|
||||
);
|
||||
process.exit(2);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
// @flow
|
||||
import chalk from 'chalk';
|
||||
import detectPort from 'detect-port';
|
||||
|
||||
(function CheckPortInUse() {
|
||||
const port: string = process.env.PORT || '1212';
|
||||
|
||||
detectPort(port, (err: ?Error, availablePort: number) => {
|
||||
if (port !== String(availablePort)) {
|
||||
throw new Error(
|
||||
chalk.whiteBright.bgRed.bold(
|
||||
`Port "${port}" on "localhost" is already in use. Please use another port. ex: PORT=4343 yarn dev`
|
||||
)
|
||||
);
|
||||
} else {
|
||||
process.exit(0);
|
||||
}
|
||||
});
|
||||
})();
|
|
@ -0,0 +1,7 @@
|
|||
// @flow
|
||||
|
||||
if (!/yarn\.js$/.test(process.env.npm_execpath || '')) {
|
||||
console.warn(
|
||||
"\u001b[33mYou don't seem to be using yarn. This could produce unexpected results.\u001b[39m"
|
||||
);
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
// @flow
|
||||
import path from 'path';
|
||||
import { execSync } from 'child_process';
|
||||
import fs from 'fs';
|
||||
import { dependencies } from '../../app/package.json';
|
||||
|
||||
(() => {
|
||||
const nodeModulesPath = path.join(
|
||||
__dirname,
|
||||
'..',
|
||||
'..',
|
||||
'app',
|
||||
'node_modules'
|
||||
);
|
||||
|
||||
if (
|
||||
Object.keys(dependencies || {}).length > 0 &&
|
||||
fs.existsSync(nodeModulesPath)
|
||||
) {
|
||||
const electronRebuildCmd =
|
||||
'../node_modules/.bin/electron-rebuild --parallel --force --types prod,dev,optional --module-dir .';
|
||||
const cmd =
|
||||
process.platform === 'win32'
|
||||
? electronRebuildCmd.replace(/\//g, '\\')
|
||||
: electronRebuildCmd;
|
||||
execSync(cmd, {
|
||||
cwd: path.join(__dirname, '..', '..', 'app'),
|
||||
stdio: 'inherit'
|
||||
});
|
||||
}
|
||||
})();
|