Upgrade electron and all dependencies
aftersignhook Add privacy level List U addrs small fixes s
|
@ -1,52 +0,0 @@
|
|||
# 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
|
|
@ -1,12 +0,0 @@
|
|||
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
|
|
@ -1,56 +0,0 @@
|
|||
# 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
|
21
.eslintrc.js
|
@ -1,21 +0,0 @@
|
|||
module.exports = {
|
||||
extends: 'erb',
|
||||
settings: {
|
||||
'import/resolver': {
|
||||
webpack: {
|
||||
config: require.resolve('./configs/webpack.config.eslint.js')
|
||||
}
|
||||
}
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: [
|
||||
"**/*.test.js"
|
||||
],
|
||||
env: {
|
||||
jest: true // now **/*.test.js files' env has both es6 *and* jest
|
||||
},
|
||||
plugins: ["jest"],
|
||||
}
|
||||
]
|
||||
}
|
26
.flowconfig
|
@ -1,26 +0,0 @@
|
|||
[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
|
|
@ -1,11 +0,0 @@
|
|||
* text eol=lf
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.jpeg binary
|
||||
*.ico binary
|
||||
*.icns binary
|
||||
zcashd binary
|
||||
zcash-cli binary
|
||||
zcashd.exe binary
|
||||
zcash-cli.exe binary
|
||||
|
|
@ -1,53 +1,24 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
# production
|
||||
/build
|
||||
/dist
|
||||
|
||||
# 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
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# 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.*
|
||||
|
||||
app/zcashd
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"overrides": [
|
||||
{
|
||||
"files": [".prettierrc", ".babelrc", ".eslintrc", ".stylelintrc"],
|
||||
"options": {
|
||||
"parser": "json"
|
||||
}
|
||||
}
|
||||
],
|
||||
"singleQuote": true,
|
||||
"printWidth": 120
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"extends": ["stylelint-config-standard", "stylelint-config-prettier"]
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
{
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Linux",
|
||||
"includePath": [
|
||||
"${workspaceFolder}/**",
|
||||
"/home/adityapk/Qt/5.13.0/gcc_64/include",
|
||||
"/home/adityapk/Qt/5.13.0/gcc_64/include/QtCore",
|
||||
"/home/adityapk/Qt/5.13.0/gcc_64/include/QtWidgets",
|
||||
"/home/adityapk/Qt/5.13.0/gcc_64/include/QtGui"
|
||||
],
|
||||
"defines": [],
|
||||
"compilerPath": "/usr/bin/clang",
|
||||
"cStandard": "c11",
|
||||
"cppStandard": "c++17",
|
||||
"intelliSenseMode": "clang-x64"
|
||||
}
|
||||
],
|
||||
"version": 4
|
||||
}
|
|
@ -3,6 +3,5 @@
|
|||
"dbaeumer.vscode-eslint",
|
||||
"dzannotti.vscode-babel-coloring",
|
||||
"EditorConfig.EditorConfig",
|
||||
"flowtype.flow-for-vscode"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -11,12 +11,10 @@
|
|||
".flowconfig": "ignore"
|
||||
},
|
||||
|
||||
"javascript.validate.enable": false,
|
||||
"javascript.validate.enable": true,
|
||||
"javascript.format.enable": false,
|
||||
"typescript.validate.enable": false,
|
||||
"typescript.validate.enable": true,
|
||||
"typescript.format.enable": false,
|
||||
|
||||
"flow.useNPMPackagedFlow": true,
|
||||
"search.exclude": {
|
||||
".git": true,
|
||||
".eslintcache": true,
|
||||
|
|
|
@ -27,9 +27,8 @@ module.exports = async function(params) {
|
|||
appleId: process.env.appleId,
|
||||
appleIdPassword: process.env.appleIdPassword
|
||||
});
|
||||
console.log(`Done notarizing ${appId}`);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
console.log(`Done notarizing ${appId}`);
|
||||
};
|
||||
|
|
|
@ -1,144 +0,0 @@
|
|||
/*
|
||||
* @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;
|
||||
}
|
48
app/app.html
|
@ -1,48 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Zecwallet Fullnode</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>
|
BIN
app/app.icns
505
app/companion.js
|
@ -1,505 +0,0 @@
|
|||
/* eslint-disable flowtype/no-weak-types */
|
||||
/* eslint-disable max-classes-per-file */
|
||||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import hex from 'hex-string';
|
||||
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, string) => void) => void = null;
|
||||
|
||||
fnUpdateConnectedClient: (string, number) => void = null;
|
||||
|
||||
permWormholeClient: WormholeClient = null;
|
||||
|
||||
tmpWormholeClient: WormholeClient = null;
|
||||
|
||||
constructor(
|
||||
fnGetSate: () => AppState,
|
||||
fnSendTransaction: ([], (string, string) => void) => void,
|
||||
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: '1.7.8'
|
||||
};
|
||||
|
||||
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();
|
||||
|
||||
const sendingAmount = parseFloat(inpTx.amount);
|
||||
|
||||
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}`);
|
||||
}
|
||||
|
||||
let memo = !inpTx.memo || inpTx.memo.trim() === '' ? null : inpTx.memo;
|
||||
const textEncoder = new TextEncoder();
|
||||
memo = memo ? hex.encode(textEncoder.encode(memo)) : '';
|
||||
|
||||
// Build a sendJSON object
|
||||
const sendJSON = [];
|
||||
sendJSON.push(fromAddress.address);
|
||||
if (memo) {
|
||||
sendJSON.push([{ address: inpTx.to, amount: sendingAmount, memo }]);
|
||||
} else {
|
||||
sendJSON.push([{ address: inpTx.to, amount: sendingAmount }]);
|
||||
}
|
||||
// console.log('sendjson is', sendJSON);
|
||||
|
||||
this.fnSendTransaction(sendJSON, (title, msg) => {
|
||||
let resp;
|
||||
if (title.startsWith('Success')) {
|
||||
const arr = msg.split(' ');
|
||||
const txid = arr && arr.length > 0 && arr[arr.length - 1];
|
||||
|
||||
resp = {
|
||||
version: 1.0,
|
||||
command: 'sendTxSubmitted',
|
||||
txid
|
||||
};
|
||||
} else {
|
||||
resp = {
|
||||
version: 1.0,
|
||||
command: 'sendTxFailed',
|
||||
err: msg
|
||||
};
|
||||
}
|
||||
|
||||
// console.log('Callback sending', resp);
|
||||
ws.send(this.encryptOutgoing(JSON.stringify(resp)));
|
||||
});
|
||||
|
||||
// After the transaction is submitted, we return an intermediate success.
|
||||
const resp = {
|
||||
version: 1.0,
|
||||
command: 'sendTx',
|
||||
result: 'success'
|
||||
};
|
||||
|
||||
// console.log('sendtx sending', resp);
|
||||
return JSON.stringify(resp);
|
||||
}
|
||||
}
|
|
@ -1,145 +0,0 @@
|
|||
/* 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.module.css';
|
||||
import cstyles from './Common.module.css';
|
||||
import { TotalBalance, Info, AddressBalance } from './AppState';
|
||||
import Utils from '../utils/utils';
|
||||
import ScrollPane from './ScrollPane';
|
||||
|
||||
// $FlowFixMe
|
||||
export const BalanceBlockHighlight = ({ zecValue, usdValue, currencyName }) => {
|
||||
const { bigPart, smallPart } = Utils.splitZecAmountIntoBigSmall(zecValue);
|
||||
|
||||
return (
|
||||
<div style={{ padding: '1em' }}>
|
||||
<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
|
||||
const BalanceBlock = ({ zecValue, usdValue, topLabel, currencyName }) => {
|
||||
const { bigPart, smallPart } = Utils.splitZecAmountIntoBigSmall(zecValue);
|
||||
|
||||
return (
|
||||
<div className={cstyles.padall}>
|
||||
<div className={[styles.sublight, styles.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>
|
||||
);
|
||||
};
|
||||
|
||||
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>{Utils.splitStringIntoChunks(item.address, 6).join(' ')}</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;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={[cstyles.well, styles.balancebox].join(' ')}>
|
||||
<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 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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
/* eslint-disable react/prop-types */
|
||||
import Modal from 'react-modal';
|
||||
import React from 'react';
|
||||
import cstyles from './Common.module.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>
|
||||
);
|
||||
};
|
|
@ -1,495 +0,0 @@
|
|||
/* eslint-disable jsx-a11y/interactive-supports-focus */
|
||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||
/* eslint-disable max-classes-per-file */
|
||||
import React, { Component } from 'react';
|
||||
import { Redirect, withRouter } from 'react-router';
|
||||
import ini from 'ini';
|
||||
import fs from 'fs';
|
||||
import request from 'request';
|
||||
import progress from 'progress-stream';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import { remote, ipcRenderer, shell } from 'electron';
|
||||
import { spawn } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import routes from '../constants/routes.json';
|
||||
import { RPCConfig, Info } from './AppState';
|
||||
import RPC from '../rpc';
|
||||
import cstyles from './Common.module.css';
|
||||
import styles from './LoadingScreen.module.css';
|
||||
import { NO_CONNECTION } from '../utils/utils';
|
||||
import Logo from '../assets/img/logobig.png';
|
||||
import zcashdlogo from '../assets/img/zcashdlogo.gif';
|
||||
|
||||
const locateZcashConfDir = () => {
|
||||
if (os.platform() === 'darwin') {
|
||||
return path.join(remote.app.getPath('appData'), 'Zcash');
|
||||
}
|
||||
|
||||
if (os.platform() === 'linux') {
|
||||
return path.join(remote.app.getPath('home'), '.zcash');
|
||||
}
|
||||
|
||||
return path.join(remote.app.getPath('appData'), 'Zcash');
|
||||
};
|
||||
|
||||
const locateZcashConf = () => {
|
||||
if (os.platform() === 'darwin') {
|
||||
return path.join(remote.app.getPath('appData'), 'Zcash', 'zcash.conf');
|
||||
}
|
||||
|
||||
if (os.platform() === 'linux') {
|
||||
return path.join(remote.app.getPath('home'), '.zcash', 'zcash.conf');
|
||||
}
|
||||
|
||||
return path.join(remote.app.getPath('appData'), 'Zcash', 'zcash.conf');
|
||||
};
|
||||
|
||||
const locateZcashd = () => {
|
||||
// const con = remote.getGlobal('console');
|
||||
// con.log(`App path = ${remote.app.getAppPath()}`);
|
||||
// con.log(`Unified = ${path.join(remote.app.getAppPath(), '..', 'bin', 'mac', 'zcashd')}`);
|
||||
|
||||
if (os.platform() === 'darwin') {
|
||||
return path.join(remote.app.getAppPath(), '..', 'bin', 'mac', 'zcashd');
|
||||
}
|
||||
|
||||
if (os.platform() === 'linux') {
|
||||
return path.join(remote.app.getAppPath(), '..', 'bin', 'linux', 'zcashd');
|
||||
}
|
||||
|
||||
return path.join(remote.app.getAppPath(), '..', 'bin', 'win', 'zcashd.exe');
|
||||
};
|
||||
|
||||
const locateZcashParamsDir = () => {
|
||||
if (os.platform() === 'darwin') {
|
||||
return path.join(remote.app.getPath('appData'), 'ZcashParams');
|
||||
}
|
||||
|
||||
if (os.platform() === 'linux') {
|
||||
return path.join(remote.app.getPath('home'), '.zcash-params');
|
||||
}
|
||||
|
||||
return path.join(remote.app.getPath('appData'), 'ZcashParams');
|
||||
};
|
||||
|
||||
type Props = {
|
||||
setRPCConfig: (rpcConfig: RPCConfig) => void,
|
||||
setInfo: (info: Info) => void,
|
||||
history: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
class LoadingScreenState {
|
||||
creatingZcashConf: boolean;
|
||||
|
||||
connectOverTor: boolean;
|
||||
|
||||
enableFastSync: boolean;
|
||||
|
||||
currentStatus: string;
|
||||
|
||||
loadingDone: boolean;
|
||||
|
||||
rpcConfig: RPCConfig | null;
|
||||
|
||||
zcashdSpawned: number;
|
||||
|
||||
getinfoRetryCount: number;
|
||||
|
||||
constructor() {
|
||||
this.currentStatus = 'Loading...';
|
||||
this.creatingZcashConf = false;
|
||||
this.loadingDone = false;
|
||||
this.zcashdSpawned = 0;
|
||||
this.getinfoRetryCount = 0;
|
||||
this.rpcConfig = null;
|
||||
}
|
||||
}
|
||||
|
||||
class LoadingScreen extends Component<Props, LoadingScreenState> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = new LoadingScreenState();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
(async () => {
|
||||
const success = await this.ensureZcashParams();
|
||||
if (success) {
|
||||
await this.loadZcashConf(true);
|
||||
await this.setupExitHandler();
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
download = (url, dest, name, cb) => {
|
||||
const file = fs.createWriteStream(dest);
|
||||
const sendReq = request.get(url);
|
||||
|
||||
// verify response code
|
||||
sendReq.on('response', response => {
|
||||
if (response.statusCode !== 200) {
|
||||
return cb(`Response status was ${response.statusCode}`);
|
||||
}
|
||||
|
||||
const totalSize = (parseInt(response.headers['content-length'], 10) / 1024 / 1024).toFixed(0);
|
||||
|
||||
const str = progress({ time: 1000 }, pgrs => {
|
||||
this.setState({
|
||||
currentStatus: `Downloading ${name}... (${(pgrs.transferred / 1024 / 1024).toFixed(0)} MB / ${totalSize} MB)`
|
||||
});
|
||||
});
|
||||
|
||||
sendReq.pipe(str).pipe(file);
|
||||
});
|
||||
|
||||
// close() is async, call cb after close completes
|
||||
file.on('finish', () => file.close(cb));
|
||||
|
||||
// check for request errors
|
||||
sendReq.on('error', err => {
|
||||
fs.unlink(dest);
|
||||
return cb(err.message);
|
||||
});
|
||||
|
||||
file.on('error', err => {
|
||||
// Handle errors
|
||||
fs.unlink(dest); // Delete the file async. (But we don't check the result)
|
||||
return cb(err.message);
|
||||
});
|
||||
};
|
||||
|
||||
ensureZcashParams = async () => {
|
||||
// Check if the zcash params dir exists and if the params files are present
|
||||
const dir = locateZcashParamsDir();
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir);
|
||||
}
|
||||
|
||||
// Check for the params
|
||||
const params = [
|
||||
{ name: 'sapling-output.params', url: 'https://params.zecwallet.co/params/sapling-output.params' },
|
||||
{ name: 'sapling-spend.params', url: 'https://params.zecwallet.co/params/sapling-spend.params' },
|
||||
{ name: 'sprout-groth16.params', url: 'https://params.zecwallet.co/params/sprout-groth16.params' }
|
||||
];
|
||||
|
||||
// eslint-disable-next-line no-plusplus
|
||||
for (let i = 0; i < params.length; i++) {
|
||||
const p = params[i];
|
||||
|
||||
const fileName = path.join(dir, p.name);
|
||||
if (!fs.existsSync(fileName)) {
|
||||
// Download and save this file
|
||||
this.setState({ currentStatus: `Downloading ${p.name}...` });
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await promisify(this.download)(p.url, fileName, p.name);
|
||||
} catch (err) {
|
||||
console.log(`error: ${err}`);
|
||||
this.setState({ currentStatus: `Error downloading ${p.name}. The error was: ${err}` });
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
async loadZcashConf(createIfMissing: boolean) {
|
||||
// Load the RPC config from zcash.conf file
|
||||
const zcashLocation = locateZcashConf();
|
||||
let confValues;
|
||||
try {
|
||||
confValues = ini.parse(await fs.promises.readFile(zcashLocation, { encoding: 'utf-8' }));
|
||||
} catch (err) {
|
||||
if (createIfMissing) {
|
||||
this.setState({ creatingZcashConf: true });
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
currentStatus: `Could not create zcash.conf at ${zcashLocation}. This is a bug, please file an issue with Zecwallet`
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the username and password
|
||||
const rpcConfig = new RPCConfig();
|
||||
rpcConfig.username = confValues.rpcuser;
|
||||
rpcConfig.password = confValues.rpcpassword;
|
||||
|
||||
if (!rpcConfig.username || !rpcConfig.password) {
|
||||
this.setState({
|
||||
currentStatus: (
|
||||
<div>
|
||||
<p>Your zcash.conf is missing a "rpcuser" or "rpcpassword".</p>
|
||||
<p>
|
||||
Please add a "rpcuser=some_username" and "rpcpassword=some_password" to your
|
||||
zcash.conf to enable RPC access
|
||||
</p>
|
||||
<p>Your zcash.conf is located at {zcashLocation}</p>
|
||||
</div>
|
||||
)
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const isTestnet = (confValues.testnet && confValues.testnet === '1') || false;
|
||||
const server = confValues.rpcbind || '127.0.0.1';
|
||||
const port = confValues.rpcport || (isTestnet ? '18232' : '8232');
|
||||
rpcConfig.url = `http://${server}:${port}`;
|
||||
|
||||
this.setState({ rpcConfig });
|
||||
|
||||
// And setup the next getinfo
|
||||
this.setupNextGetInfo();
|
||||
}
|
||||
|
||||
createZcashconf = async () => {
|
||||
const { connectOverTor, enableFastSync } = this.state;
|
||||
|
||||
const dir = locateZcashConfDir();
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir);
|
||||
}
|
||||
|
||||
const zcashConfPath = await locateZcashConf();
|
||||
|
||||
let confContent = '';
|
||||
confContent += 'server=1\n';
|
||||
confContent += 'rpcuser=zecwallet\n';
|
||||
confContent += `rpcpassword=${Math.random()
|
||||
.toString(36)
|
||||
.substring(2, 15)}\n`;
|
||||
|
||||
if (connectOverTor) {
|
||||
confContent += 'proxy=127.0.0.1:9050\n';
|
||||
}
|
||||
|
||||
if (enableFastSync) {
|
||||
confContent += 'ibdskiptxverification=1\n';
|
||||
}
|
||||
|
||||
await fs.promises.writeFile(zcashConfPath, confContent);
|
||||
|
||||
this.setState({ creatingZcashConf: false });
|
||||
this.loadZcashConf(false);
|
||||
};
|
||||
|
||||
zcashd: ChildProcessWithoutNullStreams | null = null;
|
||||
|
||||
setupExitHandler = () => {
|
||||
// App is quitting, exit zcashd as well
|
||||
ipcRenderer.on('appquitting', async () => {
|
||||
if (this.zcashd) {
|
||||
const { history } = this.props;
|
||||
const { rpcConfig } = this.state;
|
||||
|
||||
this.setState({ currentStatus: 'Waiting for zcashd to exit...' });
|
||||
history.push(routes.LOADING);
|
||||
|
||||
this.zcashd.on('close', () => {
|
||||
ipcRenderer.send('appquitdone');
|
||||
});
|
||||
this.zcashd.on('exit', () => {
|
||||
ipcRenderer.send('appquitdone');
|
||||
});
|
||||
|
||||
console.log('Sending stop');
|
||||
setTimeout(() => {
|
||||
RPC.doRPC('stop', [], rpcConfig);
|
||||
});
|
||||
} else {
|
||||
// And reply that we're all done.
|
||||
ipcRenderer.send('appquitdone');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
startZcashd = async () => {
|
||||
const { zcashdSpawned } = this.state;
|
||||
|
||||
if (zcashdSpawned) {
|
||||
this.setState({ currentStatus: 'zcashd start failed' });
|
||||
return;
|
||||
}
|
||||
|
||||
const program = locateZcashd();
|
||||
console.log(program);
|
||||
|
||||
this.zcashd = spawn(program);
|
||||
|
||||
this.setState({ zcashdSpawned: 1 });
|
||||
this.setState({ currentStatus: 'zcashd starting...' });
|
||||
|
||||
this.zcashd.on('error', err => {
|
||||
console.log(`zcashd start error, giving up. Error: ${err}`);
|
||||
// Set that we tried to start zcashd, and failed
|
||||
this.setState({ zcashdSpawned: 1 });
|
||||
|
||||
// No point retrying.
|
||||
this.setState({ getinfoRetryCount: 10 });
|
||||
});
|
||||
};
|
||||
|
||||
setupNextGetInfo() {
|
||||
setTimeout(() => this.getInfo(), 1000);
|
||||
}
|
||||
|
||||
async getInfo() {
|
||||
const { rpcConfig, zcashdSpawned, getinfoRetryCount } = this.state;
|
||||
|
||||
// Try getting the info.
|
||||
try {
|
||||
const info = await RPC.getInfoObject(rpcConfig);
|
||||
console.log(info);
|
||||
|
||||
const { setRPCConfig, setInfo } = this.props;
|
||||
|
||||
setRPCConfig(rpcConfig);
|
||||
setInfo(info);
|
||||
|
||||
// This will cause a redirect to the dashboard
|
||||
this.setState({ loadingDone: true });
|
||||
} catch (err) {
|
||||
// Not yet finished loading. So update the state, and setup the next refresh
|
||||
this.setState({ currentStatus: err });
|
||||
|
||||
if (err === NO_CONNECTION && !zcashdSpawned) {
|
||||
// Try to start zcashd
|
||||
this.startZcashd();
|
||||
this.setupNextGetInfo();
|
||||
}
|
||||
|
||||
if (err === NO_CONNECTION && zcashdSpawned && getinfoRetryCount < 10) {
|
||||
this.setState({ currentStatus: 'Waiting for zcashd to start...' });
|
||||
const inc = getinfoRetryCount + 1;
|
||||
this.setState({ getinfoRetryCount: inc });
|
||||
this.setupNextGetInfo();
|
||||
}
|
||||
|
||||
if (err === NO_CONNECTION && zcashdSpawned && getinfoRetryCount >= 10) {
|
||||
// Give up
|
||||
this.setState({
|
||||
currentStatus: (
|
||||
<span>
|
||||
Failed to start zcashd. Giving up! Please look at the debug.log file.
|
||||
<br />
|
||||
<span className={cstyles.highlight}>{`${locateZcashConfDir()}/debug.log`}</span>
|
||||
<br />
|
||||
Please file an issue with Zecwallet
|
||||
</span>
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
if (err !== NO_CONNECTION) {
|
||||
this.setupNextGetInfo();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleEnableFastSync = event => {
|
||||
this.setState({ enableFastSync: event.target.checked });
|
||||
};
|
||||
|
||||
handleTorEnabled = event => {
|
||||
this.setState({ connectOverTor: event.target.checked });
|
||||
};
|
||||
|
||||
render() {
|
||||
const { loadingDone, currentStatus, creatingZcashConf, connectOverTor, enableFastSync } = this.state;
|
||||
|
||||
// If still loading, show the status
|
||||
if (!loadingDone) {
|
||||
return (
|
||||
<div className={[cstyles.center, styles.loadingcontainer].join(' ')}>
|
||||
{!creatingZcashConf && (
|
||||
<div className={cstyles.verticalflex}>
|
||||
<div style={{ marginTop: '100px' }}>
|
||||
<img src={Logo} width="200px;" alt="Logo" />
|
||||
</div>
|
||||
<div>{currentStatus}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{creatingZcashConf && (
|
||||
<div>
|
||||
<div className={cstyles.verticalflex}>
|
||||
<div
|
||||
className={[cstyles.verticalflex, cstyles.center, cstyles.margintoplarge, cstyles.highlight].join(
|
||||
' '
|
||||
)}
|
||||
>
|
||||
<div className={[cstyles.xlarge].join(' ')}> Welcome To Zecwallet Fullnode!</div>
|
||||
</div>
|
||||
|
||||
<div className={[cstyles.center, cstyles.margintoplarge].join(' ')}>
|
||||
<img src={zcashdlogo} width="400px" alt="zcashdlogo" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={[cstyles.verticalflex, cstyles.center, cstyles.margintoplarge].join(' ')}
|
||||
style={{ width: '75%', marginLeft: '15%' }}
|
||||
>
|
||||
<div>
|
||||
Zecwallet Fullnode will download the{' '}
|
||||
<span className={cstyles.highlight}>entire Zcash Blockchain (~28GB)</span>, which might take several
|
||||
days to sync. If you want to get started immediately, please consider{' '}
|
||||
<a
|
||||
className={cstyles.highlight}
|
||||
style={{ textDecoration: 'underline' }}
|
||||
role="link"
|
||||
onClick={() => shell.openExternal('https://www.zecwallet.co')}
|
||||
>
|
||||
Zecwallet Lite
|
||||
</a>
|
||||
, which can get you started in under a minute.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={cstyles.left} style={{ width: '75%', marginLeft: '15%' }}>
|
||||
<div className={cstyles.margintoplarge} />
|
||||
<div className={[cstyles.verticalflex].join(' ')}>
|
||||
<div>
|
||||
<input type="checkbox" onChange={this.handleTorEnabled} defaultChecked={connectOverTor} />
|
||||
Connect over Tor
|
||||
</div>
|
||||
<div className={cstyles.sublight}>
|
||||
Will connect over Tor. Please make sure you have the Tor client installed and listening on port
|
||||
9050.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={cstyles.margintoplarge} />
|
||||
<div className={[cstyles.verticalflex].join(' ')}>
|
||||
<div>
|
||||
<input type="checkbox" onChange={this.handleEnableFastSync} defaultChecked={enableFastSync} />
|
||||
Enable Fast Sync
|
||||
</div>
|
||||
<div className={cstyles.sublight}>
|
||||
When enabled, Zecwallet will skip some expensive verifications of the zcashd blockchain when
|
||||
downloading. This option is safe to use if you are creating a brand new wallet.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={cstyles.buttoncontainer}>
|
||||
<button type="button" className={cstyles.primarybutton} onClick={this.createZcashconf}>
|
||||
Start Zcash
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <Redirect to={routes.DASHBOARD} />;
|
||||
}
|
||||
}
|
||||
|
||||
export default withRouter(LoadingScreen);
|
|
@ -1,296 +0,0 @@
|
|||
/* eslint-disable react/prop-types */
|
||||
import React, { Component, useState, useEffect } 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.module.css';
|
||||
import cstyles from './Common.module.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,
|
||||
viewKey,
|
||||
fetchAndSetSingleViewKey
|
||||
}) => {
|
||||
const { address } = addressBalance;
|
||||
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [timerID, setTimerID] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (timerID) {
|
||||
clearTimeout(timerID);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
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, cstyles.flex].join(' ')}
|
||||
style={{ maxWidth: '600px' }}
|
||||
>
|
||||
<QRCode value={privateKey} className={[styles.receiveQrcode].join(' ')} />
|
||||
<div>{privateKey}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={[cstyles.margintoplarge, cstyles.breakword].join(' ')}>
|
||||
{viewKey && (
|
||||
<div>
|
||||
<div className={[cstyles.sublight].join(' ')}>Viewing Key</div>
|
||||
<div
|
||||
className={[cstyles.breakword, cstyles.padtopsmall, cstyles.fixedfont, cstyles.flex].join(' ')}
|
||||
style={{ maxWidth: '600px' }}
|
||||
>
|
||||
<QRCode value={viewKey} className={[styles.receiveQrcode].join(' ')} />
|
||||
<div>{viewKey}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
className={[cstyles.primarybutton, cstyles.margintoplarge].join(' ')}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
clipboard.writeText(address);
|
||||
setCopied(true);
|
||||
setTimerID(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.isZaddr(address) && !viewKey && (
|
||||
<button
|
||||
className={[cstyles.primarybutton].join(' ')}
|
||||
type="button"
|
||||
onClick={() => fetchAndSetSingleViewKey(address)}
|
||||
>
|
||||
Export Viewing 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,
|
||||
fetchAndSetSingleViewKey: string => void,
|
||||
createNewAddress: boolean => void,
|
||||
rerenderKey: number
|
||||
};
|
||||
|
||||
export default class Receive extends Component<Props> {
|
||||
render() {
|
||||
const {
|
||||
addresses,
|
||||
addressesWithBalance,
|
||||
addressPrivateKeys,
|
||||
addressViewKeys,
|
||||
addressBook,
|
||||
info,
|
||||
receivePageState,
|
||||
fetchAndSetSinglePrivKey,
|
||||
fetchAndSetSingleViewKey,
|
||||
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]}
|
||||
viewKey={addressViewKeys[a.address]}
|
||||
fetchAndSetSinglePrivKey={fetchAndSetSinglePrivKey}
|
||||
fetchAndSetSingleViewKey={fetchAndSetSingleViewKey}
|
||||
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]}
|
||||
viewKey={addressViewKeys[a.address]}
|
||||
fetchAndSetSinglePrivKey={fetchAndSetSinglePrivKey}
|
||||
fetchAndSetSingleViewKey={fetchAndSetSingleViewKey}
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,636 +0,0 @@
|
|||
/* 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 Select from 'react-select';
|
||||
import TextareaAutosize from 'react-textarea-autosize';
|
||||
import hex from 'hex-string';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import styles from './Send.module.css';
|
||||
import cstyles from './Common.module.css';
|
||||
import { ToAddr, AddressBalance, SendPageState, Info, AddressBookEntry } 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 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';
|
||||
}
|
||||
if (toaddr.amount < 10 ** -8) {
|
||||
amountError = 'Amount is too small';
|
||||
}
|
||||
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';
|
||||
}
|
||||
|
||||
let buttonstate;
|
||||
if (
|
||||
!addressIsValid ||
|
||||
amountError ||
|
||||
!memoIsValid ||
|
||||
toaddr.to === '' ||
|
||||
parseFloat(toaddr.amount) === 0 ||
|
||||
fromAmount === 0
|
||||
) {
|
||||
buttonstate = false;
|
||||
} else {
|
||||
buttonstate = true;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
setSendButtonEnable(buttonstate);
|
||||
}, 10);
|
||||
|
||||
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"
|
||||
step="any"
|
||||
className={cstyles.inputbox}
|
||||
value={isNaN(toaddr.amount) ? '' : 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, info: Info): [] {
|
||||
const json = [];
|
||||
json.push(sendPageState.fromaddr);
|
||||
json.push(
|
||||
sendPageState.toaddrs.map(to => {
|
||||
const textEncoder = new TextEncoder();
|
||||
const memo = to.memo ? hex.encode(textEncoder.encode(to.memo)) : '';
|
||||
if (memo === '') {
|
||||
return { address: to.to, amount: to.amount };
|
||||
} else {
|
||||
return { address: to.to, amount: to.amount, memo };
|
||||
}
|
||||
})
|
||||
);
|
||||
json.push(1); // minconf = 1
|
||||
json.push(Utils.getDefaultFee(info.latestBlock)); // Control the default fee as well.
|
||||
|
||||
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,
|
||||
history
|
||||
}) => {
|
||||
const sendingTotal =
|
||||
sendPageState.toaddrs.reduce((s, t) => parseFloat(s) + parseFloat(t.amount), 0.0) +
|
||||
Utils.getDefaultFee(info.latestBlock);
|
||||
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');
|
||||
|
||||
// Then send the Tx async
|
||||
(async () => {
|
||||
const sendJson = getSendManyJSON(sendPageState, info);
|
||||
let success = false;
|
||||
|
||||
try {
|
||||
success = await sendTransaction(sendJson, openErrorModal);
|
||||
} catch (err) {
|
||||
// If there was an error, show the error modal
|
||||
openErrorModal('Error Sending Transaction', err);
|
||||
}
|
||||
|
||||
// If the Tx was sent, then clear the addresses
|
||||
if (success) {
|
||||
clearToAddrs();
|
||||
|
||||
// Redirect to dashboard after
|
||||
history.push(routes.DASHBOARD);
|
||||
}
|
||||
})();
|
||||
};
|
||||
|
||||
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: Utils.getDefaultFee(info.latestBlock), memo: null }}
|
||||
info={info}
|
||||
/>
|
||||
|
||||
{info && info.disconnected && (
|
||||
<div className={[cstyles.red, cstyles.margintoplarge].join(' ')}>
|
||||
You are currently disconnected. This transaction might not work.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{info && info.verificationprogress < 0.9999 && (
|
||||
<div className={[cstyles.red, cstyles.margintoplarge].join(' ')}>
|
||||
You are currently syncing. This transaction might not work.
|
||||
</div>
|
||||
)}
|
||||
|
||||
<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 = {
|
||||
addressesWithBalance: AddressBalance[],
|
||||
addressBook: AddressBookEntry[],
|
||||
sendPageState: SendPageState,
|
||||
sendTransaction: (sendJson: [], (string, string) => void) => void,
|
||||
setSendPageState: (sendPageState: SendPageState) => void,
|
||||
openErrorModal: (title: string, body: string) => void,
|
||||
closeErrorModal: () => void,
|
||||
info: Info
|
||||
};
|
||||
|
||||
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, info } = 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 += Utils.getDefaultFee(info.latestBlock);
|
||||
|
||||
// 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.maxPrecisionTrimmed(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 { sendPageState, info, openErrorModal, closeErrorModal } = this.props;
|
||||
|
||||
const customStyles = {
|
||||
option: (provided, state) => ({
|
||||
...provided,
|
||||
color: state.isSelected ? '#c3921f;' : 'white',
|
||||
background: '#212124;',
|
||||
padding: 20
|
||||
}),
|
||||
menu: provided => ({
|
||||
...provided,
|
||||
background: '#212124;'
|
||||
}),
|
||||
control: () => ({
|
||||
// none of react-select's styles are passed to <Control />
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'flex',
|
||||
background: '#212124;'
|
||||
}),
|
||||
singleValue: (provided, state) => {
|
||||
const opacity = state.isDisabled ? 0.5 : 1;
|
||||
const transition = 'opacity 300ms';
|
||||
|
||||
return { ...provided, opacity, transition, color: '#ffffff' };
|
||||
}
|
||||
};
|
||||
|
||||
const { addressesWithBalance, sendTransaction } = this.props;
|
||||
const sendFromList = addressesWithBalance.map(ab => {
|
||||
return {
|
||||
value: ab.address,
|
||||
label: this.getLabelForFromAddress(ab.address, addressesWithBalance, info.currencyName)
|
||||
};
|
||||
});
|
||||
|
||||
// Find the fromaddress
|
||||
let fromaddr = {};
|
||||
if (sendPageState.fromaddr) {
|
||||
fromaddr = {
|
||||
value: sendPageState.fromaddr,
|
||||
label: this.getLabelForFromAddress(sendPageState.fromaddr, addressesWithBalance, info.currencyName)
|
||||
};
|
||||
}
|
||||
|
||||
const totalAmountAvailable = this.getBalanceForAddress(fromaddr.value, addressesWithBalance);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={[cstyles.xlarge, cstyles.padall, cstyles.center].join(' ')}>Send</div>
|
||||
|
||||
<div className={styles.sendcontainer}>
|
||||
<div className={[cstyles.well, cstyles.verticalflex].join(' ')}>
|
||||
<div className={[cstyles.sublight, cstyles.padbottomsmall].join(' ')}>Send From</div>
|
||||
<Select
|
||||
value={fromaddr}
|
||||
options={sendFromList}
|
||||
styles={customStyles}
|
||||
// $FlowFixMe
|
||||
onChange={this.changeFrom}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Spacer />
|
||||
|
||||
<ScrollPane offsetHeight={300}>
|
||||
{sendPageState.toaddrs.map(toaddr => {
|
||||
return (
|
||||
<ToAddrBox
|
||||
key={toaddr.id}
|
||||
toaddr={toaddr}
|
||||
zecPrice={info.zecPrice}
|
||||
updateToField={this.updateToField}
|
||||
fromAddress={fromaddr.value}
|
||||
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}
|
||||
/>
|
||||
|
||||
<ErrorModal
|
||||
title={errorModalTitle}
|
||||
body={errorModalBody}
|
||||
modalIsOpen={errorModalIsOpen}
|
||||
closeModal={closeErrorModal}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,348 +0,0 @@
|
|||
/* 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 './Dashboard';
|
||||
import styles from './Transactions.module.css';
|
||||
import cstyles from './Common.module.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, info, 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(new ZcashURITarget(address, Utils.getDefaultFee(info.latestBlock), 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>
|
||||
|
||||
{type === 'send' && (
|
||||
<div>
|
||||
<div className={[cstyles.sublight].join(' ')}>Fees</div>
|
||||
<div>ZEC {Utils.maxPrecisionTrimmed(tx.fee)}</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]+/);
|
||||
if (split && split.length > 0 && Utils.isSapling(split[split.length - 1])) {
|
||||
replyTo = split[split.length - 1];
|
||||
}
|
||||
}
|
||||
|
||||
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: (targets: ZcashURITarget[] | ZcashURITarget) => void
|
||||
};
|
||||
|
||||
type State = {
|
||||
clickedTx: Transaction | null,
|
||||
modalIsOpen: boolean,
|
||||
numTxnsToShow: number
|
||||
};
|
||||
|
||||
export default class Transactions extends Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = { clickedTx: null, modalIsOpen: false, numTxnsToShow: 100 };
|
||||
}
|
||||
|
||||
txClicked = (tx: Transaction) => {
|
||||
// Show the modal
|
||||
if (!tx) return;
|
||||
this.setState({ clickedTx: tx, modalIsOpen: true });
|
||||
};
|
||||
|
||||
closeModal = () => {
|
||||
this.setState({ clickedTx: null, modalIsOpen: false });
|
||||
};
|
||||
|
||||
show100MoreTxns = () => {
|
||||
const { numTxnsToShow } = this.state;
|
||||
|
||||
this.setState({ numTxnsToShow: numTxnsToShow + 100 });
|
||||
};
|
||||
|
||||
render() {
|
||||
const { transactions, info, addressBook, setSendTo } = this.props;
|
||||
const { clickedTx, modalIsOpen, numTxnsToShow } = this.state;
|
||||
|
||||
const isLoadMoreEnabled = transactions && numTxnsToShow < transactions.length;
|
||||
|
||||
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.slice(0, numTxnsToShow).map(t => {
|
||||
const key = t.type + t.txid + t.address + t.index;
|
||||
return (
|
||||
<TxItemBlock
|
||||
key={key}
|
||||
transaction={t}
|
||||
currencyName={info.currencyName}
|
||||
zecPrice={info.zecPrice}
|
||||
txClicked={this.txClicked}
|
||||
addressBookMap={addressBookMap}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{isLoadMoreEnabled && (
|
||||
<div
|
||||
style={{ marginLeft: '45%', width: '100px' }}
|
||||
className={cstyles.primarybutton}
|
||||
onClick={this.show100MoreTxns}
|
||||
>
|
||||
Load more
|
||||
</div>
|
||||
)}
|
||||
</ScrollPane>
|
||||
|
||||
<TxModal
|
||||
modalIsOpen={modalIsOpen}
|
||||
tx={clickedTx}
|
||||
info={info}
|
||||
closeModal={this.closeModal}
|
||||
currencyName={info.currencyName}
|
||||
zecPrice={info.zecPrice}
|
||||
setSendTo={setSendTo}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,110 +0,0 @@
|
|||
import React, { PureComponent } from 'react';
|
||||
import QRCode from 'qrcode.react';
|
||||
import dateformat from 'dateformat';
|
||||
import cstyles from './Common.module.css';
|
||||
import styles from './WormholeConnection.module.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} level="M" 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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
.wormholeqr {
|
||||
width: 400px;
|
||||
padding: 10px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
// @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}</>;
|
||||
}
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
// @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);
|
14
app/index.js
|
@ -1,14 +0,0 @@
|
|||
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')
|
||||
);
|
152
app/main.dev.js
|
@ -1,152 +0,0 @@
|
|||
/* 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) {
|
||||
console.log('proceed to close, so closing');
|
||||
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 10 seconds
|
||||
setTimeout(() => {
|
||||
waitingForClose = false;
|
||||
proceedToClose = true;
|
||||
console.log('Timeout, quitting');
|
||||
|
||||
app.quit();
|
||||
}, 10 * 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();
|
||||
});
|
|
@ -1 +0,0 @@
|
|||
/*! http://mths.be/fromcodepoint v0.1.0 by @mathias */
|
326
app/menu.js
|
@ -1,326 +0,0 @@
|
|||
// @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);
|
||||
|
||||
const selectionMenu = Menu.buildFromTemplate([{ role: 'copy' }, { type: 'separator' }, { role: 'selectall' }]);
|
||||
|
||||
const inputMenu = Menu.buildFromTemplate([
|
||||
{ role: 'undo' },
|
||||
{ role: 'redo' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'cut' },
|
||||
{ role: 'copy' },
|
||||
{ role: 'paste' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'selectall' }
|
||||
]);
|
||||
|
||||
this.mainWindow.webContents.on('context-menu', (e, props) => {
|
||||
const { selectionText, isEditable } = props;
|
||||
if (isEditable) {
|
||||
inputMenu.popup(this.mainWindow);
|
||||
} else if (selectionText && selectionText.trim() !== '') {
|
||||
selectionMenu.popup(this.mainWindow);
|
||||
} else if (process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true') {
|
||||
const { x, y } = props;
|
||||
|
||||
Menu.buildFromTemplate([
|
||||
{
|
||||
label: 'Inspect element',
|
||||
click: () => {
|
||||
this.mainWindow.inspectElement(x, y);
|
||||
}
|
||||
}
|
||||
]).popup(this.mainWindow);
|
||||
}
|
||||
});
|
||||
|
||||
return menu;
|
||||
}
|
||||
|
||||
buildDarwinTemplate() {
|
||||
const { mainWindow } = this;
|
||||
|
||||
const subMenuAbout = {
|
||||
label: 'Zecwallet Fullnode',
|
||||
submenu: [
|
||||
{
|
||||
label: 'About Zecwallet Fullnode',
|
||||
selector: 'orderFrontStandardAboutPanel:',
|
||||
click: () => {
|
||||
mainWindow.webContents.send('about');
|
||||
}
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{ label: 'Services', submenu: [] },
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Hide Zecwallet Fullnode',
|
||||
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 subMenuFile = {
|
||||
label: 'File',
|
||||
submenu: [
|
||||
{
|
||||
label: '&Pay URI',
|
||||
accelerator: 'Ctrl+P',
|
||||
click: () => {
|
||||
mainWindow.webContents.send('payuri');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '&Import Private Keys',
|
||||
click: () => {
|
||||
mainWindow.webContents.send('import');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '&Export All Private Keys',
|
||||
click: () => {
|
||||
mainWindow.webContents.send('exportall');
|
||||
}
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Export All &Transactions',
|
||||
click: () => {
|
||||
mainWindow.webContents.send('exportalltx');
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
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: 'View',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Connect Mobile App',
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('connectmobile');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'zcashd info',
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('zcashd');
|
||||
}
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Toggle Developer Tools',
|
||||
accelerator: 'Alt+Command+I',
|
||||
click: () => {
|
||||
this.mainWindow.toggleDevTools();
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
const subMenuViewProd = {
|
||||
label: 'View',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Connect Mobile App',
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('connectmobile');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'zcashd info',
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('zcashd');
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
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/zcashfoundation/zecwallet/releases');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'File a bug...',
|
||||
click() {
|
||||
shell.openExternal('https://github.com/zcashfoundation/zecwallet/issues');
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const subMenuView = process.env.NODE_ENV === 'development' ? subMenuViewDev : subMenuViewProd;
|
||||
|
||||
return [subMenuAbout, subMenuFile, 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: '&Import Private Keys...',
|
||||
click: () => {
|
||||
mainWindow.webContents.send('import');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '&Export All Private Keys',
|
||||
click: () => {
|
||||
mainWindow.webContents.send('exportall');
|
||||
}
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Export All &Transactions',
|
||||
click: () => {
|
||||
mainWindow.webContents.send('exportalltx');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '&Close',
|
||||
accelerator: 'Ctrl+W',
|
||||
click: () => {
|
||||
this.mainWindow.close();
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '&View',
|
||||
submenu: [
|
||||
// {
|
||||
// label: 'Toggle &Developer Tools',
|
||||
// accelerator: 'Alt+Ctrl+I',
|
||||
// click: () => {
|
||||
// this.mainWindow.toggleDevTools();
|
||||
// }
|
||||
// },
|
||||
{
|
||||
label: 'Connect Mobile App',
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('connectmobile');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'zcashd info',
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('zcashd');
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Help',
|
||||
submenu: [
|
||||
{
|
||||
label: 'About Zecwallet Fullnode',
|
||||
click: () => {
|
||||
mainWindow.webContents.send('about');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Donate',
|
||||
click() {
|
||||
mainWindow.webContents.send('donate');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Check github.com for updates',
|
||||
click() {
|
||||
shell.openExternal('https://github.com/zcashfoundation/zecwallet/releases');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'File a bug...',
|
||||
click() {
|
||||
shell.openExternal('https://github.com/zcashfoundation/zecwallet/issues');
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
return templateDefault;
|
||||
}
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
"name": "electron-react-boilerplate",
|
||||
"version": "0.18.1",
|
||||
"lockfileVersion": 1
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
{
|
||||
"name": "zecwallet",
|
||||
"productName": "Zecwallet Fullnode",
|
||||
"version": "1.7.8",
|
||||
"description": "Zecwallet Fullnode",
|
||||
"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": {}
|
||||
}
|
549
app/rpc.js
|
@ -1,549 +0,0 @@
|
|||
/* eslint-disable max-classes-per-file */
|
||||
import axios from 'axios';
|
||||
import _ from 'underscore';
|
||||
import hex from 'hex-string';
|
||||
import { TotalBalance, AddressBalance, Transaction, RPCConfig, TxDetail, Info } from './components/AppState';
|
||||
import Utils, { NO_CONNECTION } from './utils/utils';
|
||||
import SentTxStore from './utils/SentTxStore';
|
||||
|
||||
const parseMemo = (memoHex: string): string | null => {
|
||||
if (!memoHex || memoHex.length < 2) return null;
|
||||
|
||||
// First, check if this is a memo (first byte is less than 'f6' (246))
|
||||
if (parseInt(memoHex.substr(0, 2), 16) >= 246) return null;
|
||||
|
||||
// Else, parse as Hex string
|
||||
const textDecoder = new TextDecoder();
|
||||
const memo = textDecoder.decode(hex.decode(memoHex));
|
||||
if (memo === '') return null;
|
||||
|
||||
return memo;
|
||||
};
|
||||
|
||||
class OpidMonitor {
|
||||
opid: string;
|
||||
|
||||
fnOpenSendErrorModal: (string, string) => void;
|
||||
}
|
||||
|
||||
export default class RPC {
|
||||
rpcConfig: RPCConfig;
|
||||
|
||||
fnSetInfo: Info => void;
|
||||
|
||||
fnSetTotalBalance: TotalBalance => void;
|
||||
|
||||
fnSetAddressesWithBalance: (AddressBalance[]) => void;
|
||||
|
||||
fnSetTransactionsList: (Transaction[]) => void;
|
||||
|
||||
fnSetAllAddresses: (string[]) => void;
|
||||
|
||||
fnSetZecPrice: number => void;
|
||||
|
||||
fnSetDisconnected: string => void;
|
||||
|
||||
opids: Set<OpidMonitor>;
|
||||
|
||||
refreshTimerID: TimerID;
|
||||
|
||||
opTimerID: TimerID;
|
||||
|
||||
priceTimerID: TimerID;
|
||||
|
||||
constructor(
|
||||
fnSetTotalBalance: TotalBalance => void,
|
||||
fnSetAddressesWithBalance: (AddressBalance[]) => void,
|
||||
fnSetTransactionsList: (Transaction[]) => void,
|
||||
fnSetAllAddresses: (string[]) => void,
|
||||
fnSetInfo: Info => void,
|
||||
fnSetZecPrice: number => void,
|
||||
fnSetDisconnected: () => void
|
||||
) {
|
||||
this.fnSetTotalBalance = fnSetTotalBalance;
|
||||
this.fnSetAddressesWithBalance = fnSetAddressesWithBalance;
|
||||
this.fnSetTransactionsList = fnSetTransactionsList;
|
||||
this.fnSetAllAddresses = fnSetAllAddresses;
|
||||
this.fnSetInfo = fnSetInfo;
|
||||
this.fnSetZecPrice = fnSetZecPrice;
|
||||
this.fnSetDisconnected = fnSetDisconnected;
|
||||
|
||||
this.opids = new Set();
|
||||
}
|
||||
|
||||
async configure(rpcConfig: RPCConfig) {
|
||||
this.rpcConfig = rpcConfig;
|
||||
|
||||
if (!this.refreshTimerID) {
|
||||
this.refreshTimerID = setTimeout(() => this.refresh(), 1000);
|
||||
}
|
||||
|
||||
if (!this.opTimerID) {
|
||||
this.opTimerID = setTimeout(() => this.refreshOpStatus(), 1000);
|
||||
}
|
||||
|
||||
if (!this.priceTimerID) {
|
||||
this.priceTimerID = setTimeout(() => this.getZecPrice(), 1000);
|
||||
}
|
||||
}
|
||||
|
||||
setupNextFetch(lastBlockHeight: number) {
|
||||
this.refreshTimerID = setTimeout(() => this.refresh(lastBlockHeight), 60 * 1000);
|
||||
}
|
||||
|
||||
static async doRPC(method: string, params: [], rpcConfig: RPCConfig) {
|
||||
const { url, username, password } = rpcConfig;
|
||||
|
||||
const response = await new Promise((resolve, reject) => {
|
||||
axios(url, {
|
||||
data: {
|
||||
jsonrpc: '2.0',
|
||||
id: 'curltest',
|
||||
method,
|
||||
params
|
||||
},
|
||||
method: 'POST',
|
||||
auth: {
|
||||
username,
|
||||
password
|
||||
}
|
||||
})
|
||||
.then(r => resolve(r.data))
|
||||
.catch(err => {
|
||||
const e = { ...err };
|
||||
if (e.response && e.response.data) {
|
||||
reject(e.response.data.error.message);
|
||||
} else {
|
||||
// eslint-disable-next-line prefer-promise-reject-errors
|
||||
reject(NO_CONNECTION);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async refresh(lastBlockHeight: number) {
|
||||
let latestBlockHeight;
|
||||
try {
|
||||
latestBlockHeight = await this.fetchInfo();
|
||||
} catch (err) {
|
||||
// If we caught an error, there's something wrong with the connection.
|
||||
this.fnSetDisconnected(`${err}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Temporary fix for zcashd stalls at 903001 issue (https://github.com/zcash/zcash/issues/4620)
|
||||
if (latestBlockHeight === 903001) {
|
||||
console.log('Looks like stalled');
|
||||
// If it is stalled at this height after 5 seconds, we will do invalidate / reconsider
|
||||
setTimeout(async () => {
|
||||
const newHeight = await this.fetchInfo();
|
||||
if (newHeight !== 903001) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Confirmed stalled');
|
||||
// First, clear the banned peers
|
||||
await RPC.doRPC('clearbanned', [], this.rpcConfig);
|
||||
// Then invalidate block
|
||||
await RPC.doRPC(
|
||||
'invalidateblock',
|
||||
['00000000006e4d348d0addad1b43ae09744f9c76a0724be4a3f5e08bdb1121ac'],
|
||||
this.rpcConfig
|
||||
);
|
||||
console.log('Invalidated block');
|
||||
|
||||
// And then, 2 seconds later, reconsider the block
|
||||
setTimeout(async () => {
|
||||
await RPC.doRPC(
|
||||
'reconsiderblock',
|
||||
['00000000006e4d348d0addad1b43ae09744f9c76a0724be4a3f5e08bdb1121ac'],
|
||||
this.rpcConfig
|
||||
);
|
||||
console.log('Reconsidered block');
|
||||
}, 2 * 1000);
|
||||
}, 5 * 1000);
|
||||
}
|
||||
|
||||
if (!lastBlockHeight || lastBlockHeight < latestBlockHeight) {
|
||||
try {
|
||||
const balP = this.fetchTotalBalance();
|
||||
const abP = this.fetchTandZAddressesWithBalances();
|
||||
const txns = this.fetchTandZTransactions();
|
||||
const addrs = this.fetchAllAddresses();
|
||||
|
||||
await balP;
|
||||
await abP;
|
||||
await txns;
|
||||
await addrs;
|
||||
|
||||
// All done, set up next fetch
|
||||
console.log(`Finished full refresh at ${latestBlockHeight}`);
|
||||
} catch (err) {
|
||||
// If we caught an error, there's something wrong with the connection.
|
||||
this.fnSetDisconnected(`${err}`);
|
||||
return;
|
||||
}
|
||||
} 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 async getInfoObject(rpcConfig: RPCConfig) {
|
||||
const infoResult = await RPC.doRPC('getinfo', [], rpcConfig);
|
||||
|
||||
const info = new Info();
|
||||
info.testnet = infoResult.result.testnet;
|
||||
info.latestBlock = infoResult.result.blocks;
|
||||
info.connections = infoResult.result.connections;
|
||||
info.version = infoResult.result.version;
|
||||
info.currencyName = info.testnet ? 'TAZ' : 'ZEC';
|
||||
info.zecPrice = null; // Setting this to null will copy over the existing price
|
||||
info.disconnected = false;
|
||||
|
||||
const blkInfoResult = await RPC.doRPC('getblockchaininfo', [], rpcConfig);
|
||||
info.verificationProgress = blkInfoResult.result.verificationprogress;
|
||||
|
||||
const solps = await RPC.doRPC('getnetworksolps', [], rpcConfig);
|
||||
info.solps = solps.result;
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
async doImportPrivKey(key: string, rescan: boolean) {
|
||||
// Z address
|
||||
if (key.startsWith('SK') || key.startsWith('secret')) {
|
||||
try {
|
||||
const r = await RPC.doRPC('z_importkey', [key, rescan ? 'yes' : 'no'], this.rpcConfig);
|
||||
console.log(r.result);
|
||||
return '';
|
||||
} catch (err) {
|
||||
return err;
|
||||
}
|
||||
} else if (key.startsWith('zxview')) {
|
||||
try {
|
||||
const r = await RPC.doRPC('z_importviewingkey', [key, rescan ? 'yes' : 'no'], this.rpcConfig);
|
||||
console.log(r.result);
|
||||
return '';
|
||||
} catch (err) {
|
||||
return err;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const r = await RPC.doRPC('importprivkey', [key, '', rescan], this.rpcConfig);
|
||||
console.log(r.result);
|
||||
return '';
|
||||
} catch (err) {
|
||||
return err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fetchInfo(): number {
|
||||
const info = await RPC.getInfoObject(this.rpcConfig);
|
||||
|
||||
this.fnSetInfo(info);
|
||||
|
||||
return info.latestBlock;
|
||||
}
|
||||
|
||||
// This method will get the total balances
|
||||
async fetchTotalBalance() {
|
||||
const response = await RPC.doRPC('z_gettotalbalance', [0], this.rpcConfig);
|
||||
|
||||
const balance = new TotalBalance();
|
||||
balance.total = response.result.total;
|
||||
balance.private = response.result.private;
|
||||
balance.transparent = response.result.transparent;
|
||||
|
||||
this.fnSetTotalBalance(balance);
|
||||
}
|
||||
|
||||
async createNewAddress(zaddress: boolean) {
|
||||
if (zaddress) {
|
||||
const newaddress = await RPC.doRPC('z_getnewaddress', ['sapling'], this.rpcConfig);
|
||||
|
||||
return newaddress.result;
|
||||
// eslint-disable-next-line no-else-return
|
||||
} else {
|
||||
const newaddress = await RPC.doRPC('getnewaddress', [''], this.rpcConfig);
|
||||
|
||||
return newaddress.result;
|
||||
}
|
||||
}
|
||||
|
||||
async getPrivKeyAsString(address: string): string {
|
||||
let method = '';
|
||||
if (Utils.isZaddr(address)) {
|
||||
method = 'z_exportkey';
|
||||
} else if (Utils.isTransparent(address)) {
|
||||
method = 'dumpprivkey';
|
||||
}
|
||||
|
||||
const response = await RPC.doRPC(method, [address], this.rpcConfig);
|
||||
|
||||
return response.result;
|
||||
}
|
||||
|
||||
async getViewKeyAsString(address: string): string {
|
||||
let method = '';
|
||||
if (Utils.isZaddr(address)) {
|
||||
method = 'z_exportviewingkey';
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
|
||||
const response = await RPC.doRPC(method, [address], this.rpcConfig);
|
||||
|
||||
return response.result;
|
||||
}
|
||||
|
||||
// Fetch all addresses and their associated balances
|
||||
async fetchTandZAddressesWithBalances() {
|
||||
const zresponse = RPC.doRPC('z_listunspent', [0], this.rpcConfig);
|
||||
const tresponse = RPC.doRPC('listunspent', [0], this.rpcConfig);
|
||||
|
||||
// Do the Z addresses
|
||||
// response.result has all the unspent notes.
|
||||
const unspentNotes = (await zresponse).result;
|
||||
const zgroups = _.groupBy(unspentNotes, 'address');
|
||||
const zaddresses = Object.keys(zgroups).map(address => {
|
||||
const balance = zgroups[address].reduce((prev, obj) => prev + obj.amount, 0);
|
||||
return new AddressBalance(address, Number(balance.toFixed(8)));
|
||||
});
|
||||
|
||||
// Do the T addresses
|
||||
const unspentTXOs = (await tresponse).result;
|
||||
const tgroups = _.groupBy(unspentTXOs, 'address');
|
||||
const taddresses = Object.keys(tgroups).map(address => {
|
||||
const balance = tgroups[address].reduce((prev, obj) => prev + obj.amount, 0);
|
||||
return new AddressBalance(address, Number(balance.toFixed(8)));
|
||||
});
|
||||
|
||||
const addresses = zaddresses.concat(taddresses);
|
||||
|
||||
this.fnSetAddressesWithBalance(addresses);
|
||||
}
|
||||
|
||||
// Fetch all T and Z transactions
|
||||
async fetchTandZTransactions() {
|
||||
const tresponse = await RPC.doRPC('listtransactions', [], this.rpcConfig);
|
||||
const zaddressesPromise = RPC.doRPC('z_listaddresses', [], this.rpcConfig);
|
||||
const senttxstorePromise = SentTxStore.loadSentTxns();
|
||||
|
||||
const ttxlist = tresponse.result.map(tx => {
|
||||
const transaction = new Transaction();
|
||||
transaction.address = tx.address;
|
||||
transaction.type = tx.category;
|
||||
transaction.amount = tx.amount;
|
||||
transaction.fee = Math.abs(tx.fee || 0);
|
||||
transaction.confirmations = tx.confirmations;
|
||||
transaction.txid = tx.txid;
|
||||
transaction.time = tx.time;
|
||||
transaction.detailedTxns = [new TxDetail()];
|
||||
transaction.detailedTxns[0].address = tx.address;
|
||||
transaction.detailedTxns[0].amount = tx.amount;
|
||||
|
||||
return transaction;
|
||||
});
|
||||
|
||||
// Now get Z txns
|
||||
const zaddresses = await zaddressesPromise;
|
||||
|
||||
const alltxnsPromise = zaddresses.result.map(async zaddr => {
|
||||
// For each zaddr, get the list of incoming transactions
|
||||
const incomingTxns = await RPC.doRPC('z_listreceivedbyaddress', [zaddr, 0], this.rpcConfig);
|
||||
const txns = incomingTxns.result
|
||||
.filter(itx => !itx.change)
|
||||
.map(incomingTx => {
|
||||
return {
|
||||
address: zaddr,
|
||||
txid: incomingTx.txid,
|
||||
memo: parseMemo(incomingTx.memo),
|
||||
amount: incomingTx.amount,
|
||||
index: incomingTx.outindex
|
||||
};
|
||||
});
|
||||
|
||||
return txns;
|
||||
});
|
||||
|
||||
const alltxns = (await Promise.all(alltxnsPromise)).flat();
|
||||
|
||||
// Now, for each tx in the array, call gettransaction
|
||||
const ztxlist = await Promise.all(
|
||||
alltxns.map(async tx => {
|
||||
const txresponse = await RPC.doRPC('gettransaction', [tx.txid], this.rpcConfig);
|
||||
|
||||
const transaction = new Transaction();
|
||||
transaction.address = tx.address;
|
||||
transaction.type = 'receive';
|
||||
transaction.amount = tx.amount;
|
||||
transaction.confirmations = txresponse.result.confirmations;
|
||||
transaction.txid = tx.txid;
|
||||
transaction.time = txresponse.result.time;
|
||||
transaction.index = tx.index || 0;
|
||||
transaction.detailedTxns = [new TxDetail()];
|
||||
transaction.detailedTxns[0].address = tx.address;
|
||||
transaction.detailedTxns[0].amount = tx.amount;
|
||||
// eslint-disable-next-line no-control-regex
|
||||
transaction.detailedTxns[0].memo = tx.memo ? tx.memo.replace(/\u0000/g, '') : tx.memo;
|
||||
|
||||
return transaction;
|
||||
})
|
||||
);
|
||||
|
||||
// Get transactions from the sent tx store
|
||||
const sentTxns = await senttxstorePromise;
|
||||
|
||||
// Now concat the t and z transactions, and call the update function again
|
||||
const alltxlist = ttxlist
|
||||
.concat(ztxlist)
|
||||
.concat(sentTxns)
|
||||
.sort((tx1, tx2) => {
|
||||
if (tx1.time && tx2.time) {
|
||||
return tx2.time - tx1.time;
|
||||
}
|
||||
|
||||
return tx1.confirmations - tx2.confirmations;
|
||||
});
|
||||
|
||||
this.fnSetTransactionsList(alltxlist);
|
||||
}
|
||||
|
||||
// Get all Addresses, including T and Z addresses
|
||||
async fetchAllAddresses() {
|
||||
const zaddrsPromise = RPC.doRPC('z_listaddresses', [], this.rpcConfig);
|
||||
const taddrs2Promise = RPC.doRPC('listaddressgroupings', [], this.rpcConfig);
|
||||
|
||||
const allZ = (await zaddrsPromise).result;
|
||||
|
||||
const t2r = await taddrs2Promise;
|
||||
const t2 = t2r.result.flatMap(a => a.flatMap(aa => aa[0]));
|
||||
const allT = t2.filter(a => Utils.isTransparent(a));
|
||||
|
||||
this.fnSetAllAddresses(allZ.concat(allT));
|
||||
}
|
||||
|
||||
// Send a transaction using the already constructed sendJson structure
|
||||
async sendTransaction(sendJson: [], fnOpenSendErrorModal: (string, string) => void): boolean {
|
||||
try {
|
||||
const opid = (await RPC.doRPC('z_sendmany', sendJson, this.rpcConfig)).result;
|
||||
|
||||
const monitor = new OpidMonitor();
|
||||
monitor.opid = opid;
|
||||
monitor.fnOpenSendErrorModal = fnOpenSendErrorModal;
|
||||
|
||||
this.addOpidToMonitor(monitor);
|
||||
|
||||
return true;
|
||||
} catch (err) {
|
||||
// TODO Show a modal with the error
|
||||
console.log(`Error sending Tx: ${err}`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// Start monitoring the given opid
|
||||
async addOpidToMonitor(monitor: OpidMonitor) {
|
||||
this.opids.add(monitor);
|
||||
this.refreshOpStatus();
|
||||
}
|
||||
|
||||
setupNextOpidSatusFetch() {
|
||||
if (this.opids.size > 0) {
|
||||
this.opTimerID = setTimeout(() => this.refreshOpStatus(), 2000); // 2 sec
|
||||
} else {
|
||||
this.opTimerID = null;
|
||||
}
|
||||
}
|
||||
|
||||
async refreshOpStatus() {
|
||||
if (this.opids.size > 0) {
|
||||
// Get all the operation statuses.
|
||||
[...this.opids].map(async monitor => {
|
||||
try {
|
||||
const resultJson = await RPC.doRPC('z_getoperationstatus', [[monitor.opid]], this.rpcConfig);
|
||||
|
||||
const result = resultJson.result[0];
|
||||
|
||||
if (result.status === 'success') {
|
||||
const { txid } = result.result;
|
||||
|
||||
monitor.fnOpenSendErrorModal(
|
||||
'Successfully Broadcast Transaction',
|
||||
`Transaction was successfully broadcast. TXID: ${txid}`
|
||||
);
|
||||
|
||||
this.opids.delete(monitor);
|
||||
|
||||
// And force a refresh to update the balances etc...
|
||||
this.refresh(null);
|
||||
} else if (result.status === 'failed') {
|
||||
monitor.fnOpenSendErrorModal(
|
||||
'Error Sending Transaction',
|
||||
`Opid ${monitor.opid} Failed. ${result.error.message}`
|
||||
);
|
||||
|
||||
this.opids.delete(monitor);
|
||||
}
|
||||
} catch (err) {
|
||||
// If we can't get a response for this OPID, then just forget it and move on
|
||||
this.opids.delete(monitor);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.setupNextOpidSatusFetch();
|
||||
}
|
||||
|
||||
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.coincap.io/v2/rates/zcash', {
|
||||
method: 'GET'
|
||||
})
|
||||
.then(r => resolve(r.data))
|
||||
.catch(err => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
|
||||
const zecData = response.data;
|
||||
if (zecData) {
|
||||
this.fnSetZecPrice(zecData.rateUsd);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
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 [];
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
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 [];
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,101 +0,0 @@
|
|||
import { parseZcashURI } from './uris';
|
||||
|
||||
test('ZIP321 case 1', () => {
|
||||
const targets = parseZcashURI(
|
||||
'zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=1&memo=VGhpcyBpcyBhIHNpbXBsZSBtZW1vLg&message=Thank%20you%20for%20your%20purchase'
|
||||
);
|
||||
|
||||
expect(targets.length).toBe(1);
|
||||
expect(targets[0].address).toBe(
|
||||
'ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez'
|
||||
);
|
||||
expect(targets[0].message).toBe('Thank you for your purchase');
|
||||
expect(targets[0].label).toBeUndefined();
|
||||
expect(targets[0].amount).toBe(1);
|
||||
expect(targets[0].memoString).toBe('This is a simple memo.');
|
||||
});
|
||||
|
||||
test('ZIP321 case 2', () => {
|
||||
const targets = parseZcashURI(
|
||||
'zcash:?address=tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU&amount=123.456&address.1=ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez&amount.1=0.789&memo.1=VGhpcyBpcyBhIHVuaWNvZGUgbWVtbyDinKjwn6aE8J-PhvCfjok'
|
||||
);
|
||||
|
||||
expect(targets.length).toBe(2);
|
||||
|
||||
expect(targets[0].address).toBe('tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU');
|
||||
expect(targets[0].message).toBeUndefined();
|
||||
expect(targets[0].label).toBeUndefined();
|
||||
expect(targets[0].amount).toBe(123.456);
|
||||
expect(targets[0].memoString).toBeUndefined();
|
||||
expect(targets[0].memoBase64).toBeUndefined();
|
||||
|
||||
expect(targets[1].address).toBe(
|
||||
'ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez'
|
||||
);
|
||||
expect(targets[1].message).toBeUndefined();
|
||||
expect(targets[1].label).toBeUndefined();
|
||||
expect(targets[1].amount).toBe(0.789);
|
||||
expect(targets[1].memoString).toBe('This is a unicode memo ✨🦄🏆🎉');
|
||||
});
|
||||
|
||||
test('coinbase URI', () => {
|
||||
const targets = parseZcashURI('zcash:tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU');
|
||||
|
||||
expect(targets.length).toBe(1);
|
||||
expect(targets[0].message).toBeUndefined();
|
||||
expect(targets[0].label).toBeUndefined();
|
||||
expect(targets[0].amount).toBeUndefined();
|
||||
expect(targets[0].memoString).toBeUndefined();
|
||||
expect(targets[0].memoBase64).toBeUndefined();
|
||||
});
|
||||
|
||||
test('Plain URI', () => {
|
||||
const targets = parseZcashURI('tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU');
|
||||
|
||||
expect(targets.length).toBe(1);
|
||||
expect(targets[0].message).toBeUndefined();
|
||||
expect(targets[0].label).toBeUndefined();
|
||||
expect(targets[0].amount).toBeUndefined();
|
||||
expect(targets[0].memoString).toBeUndefined();
|
||||
expect(targets[0].memoBase64).toBeUndefined();
|
||||
});
|
||||
|
||||
test('bad uris', () => {
|
||||
// bad protocol
|
||||
let error = parseZcashURI('badprotocol:tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU?amount=123.456');
|
||||
expect(typeof error).toBe('string');
|
||||
|
||||
// bad address
|
||||
error = parseZcashURI('zcash:badaddress?amount=123.456');
|
||||
expect(typeof error).toBe('string');
|
||||
|
||||
// no address
|
||||
error = parseZcashURI('zcash:?amount=123.456');
|
||||
expect(typeof error).toBe('string');
|
||||
|
||||
// bad param name
|
||||
error = parseZcashURI('zcash:tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU?badparam=3');
|
||||
expect(typeof error).toBe('string');
|
||||
|
||||
// index=1 doesn't have amount
|
||||
error = parseZcashURI(
|
||||
'zcash:tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU?amount=2&address.1=tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU'
|
||||
);
|
||||
expect(typeof error).toBe('string');
|
||||
|
||||
// duplicate param
|
||||
error = parseZcashURI('zcash:tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU?amount=3&amount=3');
|
||||
expect(typeof error).toBe('string');
|
||||
|
||||
// bad index
|
||||
error = parseZcashURI(
|
||||
'zcash:tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU?amount=2&address.a=tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU&amount.a=3'
|
||||
);
|
||||
expect(typeof error).toBe('string');
|
||||
|
||||
// index=1 is missing
|
||||
error = parseZcashURI(
|
||||
'zcash:tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU?amount=0.1&address.2=tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU&amount.2=2'
|
||||
);
|
||||
expect(typeof error).toBe('string');
|
||||
});
|
|
@ -1,4 +0,0 @@
|
|||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
39
appveyor.yml
|
@ -1,39 +0,0 @@
|
|||
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
|
|
@ -1,66 +0,0 @@
|
|||
/* 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)
|
||||
]
|
||||
};
|
||||
};
|
BIN
bin/linux/zcashd
BIN
bin/mac/zcashd
|
@ -1 +0,0 @@
|
|||
echo "VERSION=1.7.8" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
|
@ -1,3 +0,0 @@
|
|||
#!/bin/bash
|
||||
VERSION="1.7.8"
|
||||
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
|
@ -1,47 +0,0 @@
|
|||
#!/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/
|
|
@ -1,12 +0,0 @@
|
|||
This directory contains the hashes and signatures for ZecWallet
|
||||
|
||||
Verify the hashes by running:
|
||||
sha256sum -c sha256sum-vX.Y.Z.txt
|
||||
|
||||
Verify signatures:
|
||||
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
|
||||
gpg --verify <filename.sig> <downloaded-filename-to-verify>
|
|
@ -1,12 +0,0 @@
|
|||
<?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>
|
|
@ -1,51 +0,0 @@
|
|||
/**
|
||||
* 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()
|
||||
]
|
||||
};
|
|
@ -1,4 +0,0 @@
|
|||
/* eslint import/no-unresolved: off, import/no-self-import: off */
|
||||
require('@babel/register');
|
||||
|
||||
module.exports = require('./webpack.config.renderer.dev.babel').default;
|
|
@ -1,73 +0,0 @@
|
|||
/**
|
||||
* 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
|
||||
}
|
||||
});
|
|
@ -1,280 +0,0 @@
|
|||
/* 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'
|
||||
}
|
||||
]
|
||||
},
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,74 +0,0 @@
|
|||
/* 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')
|
||||
}
|
||||
}
|
||||
})
|
||||
]
|
||||
});
|
|
@ -1,218 +0,0 @@
|
|||
/**
|
||||
* 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'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
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,3 +0,0 @@
|
|||
declare module 'module' {
|
||||
declare module.exports: any;
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
// @flow
|
||||
|
||||
declare export default { [key: string]: string }
|
|
@ -1,2 +0,0 @@
|
|||
// @flow
|
||||
declare export default string
|
Before Width: | Height: | Size: 79 KiB |
Before Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 6.6 KiB |
Before Width: | Height: | Size: 9.5 KiB |
Before Width: | Height: | Size: 9.9 KiB |
Before Width: | Height: | Size: 5.3 KiB |
Before Width: | Height: | Size: 5.6 KiB |
Before Width: | Height: | Size: 6.2 KiB |
Before Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 5.0 KiB |
Before Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 5.0 KiB |
Before Width: | Height: | Size: 6.4 KiB |
Before Width: | Height: | Size: 4.7 KiB |
Before Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 5.5 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 4.9 KiB |
Before Width: | Height: | Size: 5.4 KiB |
Before Width: | Height: | Size: 6.1 KiB |
Before Width: | Height: | Size: 6.5 KiB |
Before Width: | Height: | Size: 2.1 KiB |
|
@ -1 +0,0 @@
|
|||
export default 'test-file-stub';
|
|
@ -1,5 +0,0 @@
|
|||
const path = require('path');
|
||||
|
||||
require('@babel/register')({
|
||||
cwd: path.join(__dirname, '..', '..')
|
||||
});
|
|
@ -1,35 +0,0 @@
|
|||
// @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();
|
|
@ -1,51 +0,0 @@
|
|||
// @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');
|
||||
}
|
||||
})();
|
|
@ -1,17 +0,0 @@
|
|||
// @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);
|
||||
}
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
// @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);
|
||||
}
|
||||
});
|
||||
})();
|
|
@ -1,7 +0,0 @@
|
|||
// @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"
|
||||
);
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
// @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'
|
||||
});
|
||||
}
|
||||
})();
|
402
package.json
|
@ -3,163 +3,57 @@
|
|||
"productName": "Zecwallet Fullnode",
|
||||
"version": "1.7.8",
|
||||
"description": "Zecwallet Fullnode (Electron version)",
|
||||
"private": true,
|
||||
"main": "./public/electron.js",
|
||||
"homepage": "./",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^6.1.1",
|
||||
"@testing-library/jest-dom": "^5.16.4",
|
||||
"@testing-library/react": "^13.3.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"axios": "^0.27.2",
|
||||
"dateformat": "^5.0.3",
|
||||
"fs": "^0.0.1-security",
|
||||
"ini": "^3.0.0",
|
||||
"js-base64": "^3.7.2",
|
||||
"path": "^0.12.7",
|
||||
"qrcode.react": "^3.0.2",
|
||||
"querystring": "^0.2.1",
|
||||
"react": "^18.2.0",
|
||||
"react-accessible-accordion": "^5.0.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-modal": "^3.15.1",
|
||||
"react-router-dom": "^6.3.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"react-select": "^5.3.2",
|
||||
"react-tabs": "^5.1.0",
|
||||
"react-textarea-autosize": "^8.3.4",
|
||||
"typeface-roboto": "^1.1.13",
|
||||
"typescript": "^4.7.3",
|
||||
"underscore": "^1.13.4",
|
||||
"url": "^0.11.0",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "concurrently \"yarn build-main\" \"yarn build-renderer\"",
|
||||
"build-dll": "cross-env NODE_ENV=development webpack --config ./configs/webpack.config.renderer.dev.dll.babel.js --colors",
|
||||
"build-e2e": "cross-env E2E_BUILD=true yarn build",
|
||||
"build-main": "cross-env NODE_ENV=production webpack --config ./configs/webpack.config.main.prod.babel.js --colors",
|
||||
"build-renderer": "cross-env NODE_ENV=production webpack --config ./configs/webpack.config.renderer.prod.babel.js --colors",
|
||||
"dev": "cross-env START_HOT=1 node -r @babel/register ./internals/scripts/CheckPortInUse.js && cross-env START_HOT=1 yarn start-renderer-dev",
|
||||
"electron-rebuild": "electron-rebuild --parallel --force --types prod,dev,optional --module-dir app",
|
||||
"flow": "flow",
|
||||
"flow-typed": "rimraf flow-typed/npm && flow-typed install --overwrite || true",
|
||||
"lint": "cross-env NODE_ENV=development eslint --cache --format=pretty .",
|
||||
"lint-fix": "yarn --silent lint --fix; exit 0",
|
||||
"lint-styles": "stylelint --ignore-path .eslintignore '**/*.*(css|scss)' --syntax scss",
|
||||
"lint-styles-fix": "yarn --silent lint-styles --fix; exit 0",
|
||||
"package": "yarn build && electron-builder build --publish never",
|
||||
"package-all": "yarn build && electron-builder build -mwl",
|
||||
"package-ci": "yarn postinstall && yarn build && electron-builder --publish always",
|
||||
"package-mac": "yarn build && electron-builder build --mac --publish=never",
|
||||
"package-linux": "yarn build && electron-builder build --linux --publish=never",
|
||||
"package-win": "yarn build && electron-builder build --win --x64 --publish=never",
|
||||
"postinstall": "node -r @babel/register internals/scripts/CheckNativeDep.js && yarn flow-typed && electron-builder install-app-deps && yarn build-dll && opencollective-postinstall",
|
||||
"postlint-fix": "prettier --ignore-path .eslintignore --single-quote --write '**/*.{js,jsx,json,html,css,less,scss,yml}'",
|
||||
"postlint-styles-fix": "prettier --ignore-path .eslintignore --single-quote --write '**/*.{css,scss}'",
|
||||
"preinstall": "node ./internals/scripts/CheckYarn.js",
|
||||
"prestart": "yarn build",
|
||||
"start": "cross-env NODE_ENV=production electron ./app/main.prod.js",
|
||||
"start-main-dev": "cross-env START_HOT=1 NODE_ENV=development electron -r @babel/register ./app/main.dev.js --remote-debugging-port=9223",
|
||||
"start-renderer-dev": "cross-env NODE_ENV=development webpack-dev-server --config configs/webpack.config.renderer.dev.babel.js",
|
||||
"test": "jest"
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject",
|
||||
"electron:prod": "yarn build && cross-env NODE_ENV=production && electron build/electron.js",
|
||||
"electron:start": "concurrently -k \"cross-env BROWSER=none yarn start\" \"wait-on http://localhost:3000 && electronmon .\"",
|
||||
"package-mac": "yarn build && electron-builder -m -c.extraMetadata.main=build/electron.js",
|
||||
"package-win": "yarn build && electron-builder -w -c.extraMetadata.main=build/electron.js",
|
||||
"package-linux": "yarn build && electron-builder -l -c.extraMetadata.main=build/electron.js"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,jsx}": [
|
||||
"cross-env NODE_ENV=development eslint --cache --format=pretty",
|
||||
"git add"
|
||||
],
|
||||
"{*.json,.{babelrc,eslintrc,prettierrc,stylelintrc}}": [
|
||||
"prettier --ignore-path .eslintignore --parser json --write",
|
||||
"git add"
|
||||
],
|
||||
"*.{css,scss}": [
|
||||
"stylelint --ignore-path .eslintignore --syntax scss --fix",
|
||||
"prettier --ignore-path .eslintignore --single-quote --write",
|
||||
"git add"
|
||||
],
|
||||
"*.{html,md,yml}": [
|
||||
"prettier --ignore-path .eslintignore --single-quote --write",
|
||||
"git add"
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"build": {
|
||||
"productName": "Zecwallet Fullnode",
|
||||
"appId": "co.zecwallet.fullnode",
|
||||
"afterAllArtifactBuild": "./afterSignHook.js",
|
||||
"files": [
|
||||
"dist/",
|
||||
"node_modules/",
|
||||
"app.html",
|
||||
"main.prod.js",
|
||||
"main.prod.js.map",
|
||||
"package.json"
|
||||
],
|
||||
"dmg": {
|
||||
"contents": [
|
||||
{
|
||||
"x": 130,
|
||||
"y": 220
|
||||
},
|
||||
{
|
||||
"x": 410,
|
||||
"y": 220,
|
||||
"type": "link",
|
||||
"path": "/Applications"
|
||||
}
|
||||
]
|
||||
},
|
||||
"win": {
|
||||
"icon": "./resources/icon.ico",
|
||||
"extraFiles": [
|
||||
{
|
||||
"from": "bin/win",
|
||||
"to": "resources/bin/win",
|
||||
"filter": [
|
||||
"**/**"
|
||||
]
|
||||
}
|
||||
],
|
||||
"target": [
|
||||
"zip",
|
||||
"msi"
|
||||
]
|
||||
},
|
||||
"mac": {
|
||||
"category": "public.app-category.productivity",
|
||||
"type": "distribution",
|
||||
"hardenedRuntime": true,
|
||||
"entitlements": "./configs/entitlements.mac.inherit.plist",
|
||||
"target": [
|
||||
"dmg"
|
||||
],
|
||||
"icon": "./resources/icon.icns",
|
||||
"extraFiles": [
|
||||
{
|
||||
"from": "bin/mac",
|
||||
"to": "resources/bin/mac",
|
||||
"filter": [
|
||||
"**/*"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"deb": {
|
||||
"depends": [
|
||||
"gconf2",
|
||||
"gconf-service",
|
||||
"libnotify4",
|
||||
"libappindicator1",
|
||||
"libxtst6",
|
||||
"libnss3"
|
||||
],
|
||||
"artifactName": "Zecwallet_Fullnode_${version}_${arch}.deb"
|
||||
},
|
||||
"linux": {
|
||||
"category": "Development",
|
||||
"icon": "./resources/icon.icns",
|
||||
"extraFiles": [
|
||||
{
|
||||
"from": "bin/linux",
|
||||
"to": "resources/bin/linux",
|
||||
"filter": [
|
||||
"**/**"
|
||||
]
|
||||
}
|
||||
],
|
||||
"target": [
|
||||
"deb",
|
||||
"AppImage"
|
||||
],
|
||||
"desktop": {
|
||||
"Name": "ZecWallet",
|
||||
"Comment": "Full node and wallet for Zcash",
|
||||
"GenericName": "Wallet",
|
||||
"Type": "Application",
|
||||
"StartupNotify": true,
|
||||
"StartupWMClass": "zecwallet",
|
||||
"Categories": "Utility;",
|
||||
"MimeType": "x-scheme-handler/zcash;",
|
||||
"Keywords": "zecwallet;"
|
||||
}
|
||||
},
|
||||
"directories": {
|
||||
"buildResources": "resources",
|
||||
"output": "release"
|
||||
}
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/adityapk00/zecwallet-electron.git"
|
||||
"url": "git+https://github.com/zcashfoundation/zecwallet.git"
|
||||
},
|
||||
"author": {
|
||||
"name": "Aditya Kulkarni",
|
||||
|
@ -174,171 +68,55 @@
|
|||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/adityapk00/zecwallet-electron/issues"
|
||||
"build": {
|
||||
"productName": "Zecwallet Fullnode",
|
||||
"appId": "co.zecwallet.fullnode",
|
||||
"afterAllArtifactBuild": "./afterSignHook.js",
|
||||
"files": [
|
||||
"build/**/*",
|
||||
"node_modules/**/*"
|
||||
],
|
||||
"directories": {
|
||||
"buildResources": "public"
|
||||
},
|
||||
"mac": {
|
||||
"target": "dmg"
|
||||
},
|
||||
"win": {
|
||||
"target": [
|
||||
"zip",
|
||||
"msi"
|
||||
]
|
||||
},
|
||||
"linux": {
|
||||
"target": "deb"
|
||||
}
|
||||
},
|
||||
"keywords": [
|
||||
"electron",
|
||||
"react",
|
||||
"flow",
|
||||
"sass",
|
||||
"webpack"
|
||||
],
|
||||
"homepage": "https://github.com/adityapk00/zecwallet-electron#readme",
|
||||
"jest": {
|
||||
"testURL": "http://localhost/",
|
||||
"moduleNameMapper": {
|
||||
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/internals/mocks/fileMock.js",
|
||||
"\\.(css|less|sass|scss)$": "identity-obj-proxy"
|
||||
},
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
"jsx",
|
||||
"json"
|
||||
"browserslist": {
|
||||
"production": [
|
||||
"last 1 electron version"
|
||||
],
|
||||
"moduleDirectories": [
|
||||
"node_modules",
|
||||
"app/node_modules"
|
||||
],
|
||||
"transform": {
|
||||
"^.+\\.jsx?$": "babel-jest"
|
||||
},
|
||||
"setupFiles": [
|
||||
"./internals/scripts/CheckBuildsExist.js"
|
||||
"development": [
|
||||
"last 1 electron version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.7.5",
|
||||
"@babel/plugin-proposal-class-properties": "^7.7.4",
|
||||
"@babel/plugin-proposal-decorators": "^7.7.4",
|
||||
"@babel/plugin-proposal-do-expressions": "^7.7.4",
|
||||
"@babel/plugin-proposal-export-default-from": "^7.7.4",
|
||||
"@babel/plugin-proposal-export-namespace-from": "^7.7.4",
|
||||
"@babel/plugin-proposal-function-bind": "^7.7.4",
|
||||
"@babel/plugin-proposal-function-sent": "^7.7.4",
|
||||
"@babel/plugin-proposal-json-strings": "^7.7.4",
|
||||
"@babel/plugin-proposal-logical-assignment-operators": "^7.7.4",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.7.4",
|
||||
"@babel/plugin-proposal-numeric-separator": "^7.7.4",
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.7.5",
|
||||
"@babel/plugin-proposal-pipeline-operator": "^7.7.4",
|
||||
"@babel/plugin-proposal-throw-expressions": "^7.7.4",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.7.4",
|
||||
"@babel/plugin-syntax-import-meta": "^7.7.4",
|
||||
"@babel/plugin-transform-react-constant-elements": "^7.7.4",
|
||||
"@babel/plugin-transform-react-inline-elements": "^7.7.4",
|
||||
"@babel/preset-env": "^7.7.6",
|
||||
"@babel/preset-flow": "^7.7.4",
|
||||
"@babel/preset-react": "^7.7.4",
|
||||
"@babel/register": "^7.7.4",
|
||||
"babel-core": "7.0.0-bridge.0",
|
||||
"babel-eslint": "^10.0.3",
|
||||
"babel-jest": "^24.9.0",
|
||||
"babel-loader": "^8.0.6",
|
||||
"babel-plugin-dev-expression": "^0.2.2",
|
||||
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",
|
||||
"chalk": "^3.0.0",
|
||||
"concurrently": "^5.0.1",
|
||||
"cross-env": "^6.0.3",
|
||||
"cross-spawn": "^7.0.1",
|
||||
"css-loader": "^3.3.2",
|
||||
"detect-port": "^1.3.0",
|
||||
"electron": "7.2.4",
|
||||
"electron-builder": "^22.2.0",
|
||||
"electron-devtools-installer": "^2.2.4",
|
||||
"electron-notarize": "^0.2.1",
|
||||
"electron-rebuild": "^1.8.8",
|
||||
"enzyme": "^3.7.0",
|
||||
"enzyme-adapter-react-16": "^1.7.0",
|
||||
"enzyme-to-json": "^3.3.4",
|
||||
"eslint": "^6.7.2",
|
||||
"eslint-config-airbnb": "^18.0.1",
|
||||
"eslint-config-erb": "^0.1.1",
|
||||
"eslint-config-prettier": "^6.6.0",
|
||||
"eslint-formatter-pretty": "^3.0.0",
|
||||
"eslint-import-resolver-webpack": "^0.12.0",
|
||||
"eslint-plugin-compat": "^3.3.0",
|
||||
"eslint-plugin-flowtype": "^4.5.2",
|
||||
"eslint-plugin-import": "^2.19.1",
|
||||
"eslint-plugin-jest": "^23.1.1",
|
||||
"eslint-plugin-jsx-a11y": "6.2.3",
|
||||
"eslint-plugin-prettier": "^3.1.1",
|
||||
"eslint-plugin-promise": "^4.2.1",
|
||||
"eslint-plugin-react": "^7.17.0",
|
||||
"eslint-plugin-testcafe": "^0.2.1",
|
||||
"fbjs-scripts": "^1.2.0",
|
||||
"file-loader": "^5.0.2",
|
||||
"flow-bin": "^0.113.0",
|
||||
"flow-runtime": "^0.17.0",
|
||||
"flow-typed": "^2.6.2",
|
||||
"husky": "^3.1.0",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "^26.5.3",
|
||||
"lint-staged": "^9.5.0",
|
||||
"mini-css-extract-plugin": "^0.8.0",
|
||||
"node-sass": "^4.13.0",
|
||||
"opencollective-postinstall": "^2.0.2",
|
||||
"optimize-css-assets-webpack-plugin": "^5.0.3",
|
||||
"prettier": "^1.19.1",
|
||||
"react-test-renderer": "^16.12.0",
|
||||
"redux-logger": "^3.0.6",
|
||||
"rimraf": "^3.0.0",
|
||||
"sass-loader": "^8.0.0",
|
||||
"sinon": "^7.5.0",
|
||||
"spectron": "^9.0.0",
|
||||
"style-loader": "^1.0.1",
|
||||
"stylelint": "^12.0.0",
|
||||
"stylelint-config-prettier": "^8.0.0",
|
||||
"stylelint-config-standard": "^19.0.0",
|
||||
"terser-webpack-plugin": "^2.3.0",
|
||||
"url-loader": "^3.0.0",
|
||||
"webpack": "^4.41.2",
|
||||
"webpack-bundle-analyzer": "^3.6.0",
|
||||
"webpack-cli": "^3.3.10",
|
||||
"webpack-dev-server": "^3.9.0",
|
||||
"webpack-merge": "^4.2.2",
|
||||
"yarn": "^1.22.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^5.12.0",
|
||||
"@hot-loader/react-dom": "^16.11.0",
|
||||
"axios": "^0.21.1",
|
||||
"dateformat": "^3.0.3",
|
||||
"electron-debug": "^3.0.1",
|
||||
"electron-log": "^4.0.0",
|
||||
"electron-store": "^5.1.1",
|
||||
"hex-string": "^1.0.3",
|
||||
"history": "^4.10.1",
|
||||
"ini": "^1.3.5",
|
||||
"js-base64": "^3.5.2",
|
||||
"js-sha256": "^0.9.0",
|
||||
"libsodium-wrappers-sumo": "^0.7.6",
|
||||
"progress-stream": "^2.0.0",
|
||||
"qrcode.react": "^1.0.0",
|
||||
"react": "^16.12.0",
|
||||
"react-accessible-accordion": "^3.0.1",
|
||||
"react-dom": "^16.12.0",
|
||||
"react-hot-loader": "^4.12.18",
|
||||
"react-modal": "^3.11.1",
|
||||
"react-router": "^5.1.2",
|
||||
"react-router-dom": "^5.1.2",
|
||||
"react-select": "^3.0.8",
|
||||
"react-tabs": "^3.1.0",
|
||||
"react-textarea-autosize": "^7.1.2",
|
||||
"source-map-support": "^0.5.16",
|
||||
"typeface-roboto": "^0.0.75",
|
||||
"underscore": "^1.9.2",
|
||||
"ws": "^7.2.1"
|
||||
},
|
||||
"devEngines": {
|
||||
"node": ">=7.x",
|
||||
"npm": ">=4.x",
|
||||
"yarn": ">=0.21.3"
|
||||
},
|
||||
"browserslist": "electron 1.6",
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "lint-staged"
|
||||
}
|
||||
"@types/dateformat": "^5.0.0",
|
||||
"@types/ini": "^1.3.31",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/js-base64": "^3.3.1",
|
||||
"@types/node": "^16.11.41",
|
||||
"@types/react": "^18.0.14",
|
||||
"@types/react-dom": "^18.0.5",
|
||||
"@types/react-modal": "^3.13.1",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@types/underscore": "^1.11.4",
|
||||
"concurrently": "^7.2.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"electron": "^19.0.4",
|
||||
"electron-builder": "^23.0.3",
|
||||
"electron-notarize": "^1.2.1",
|
||||
"electronmon": "^2.0.2",
|
||||
"wait-on": "^6.0.1"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,165 @@
|
|||
// Module to control the application lifecycle and the native browser window.
|
||||
const { app, BrowserWindow, protocol, ipcMain, dialog } = require("electron");
|
||||
const path = require("path");
|
||||
const url = require("url");
|
||||
const axios = require("axios");
|
||||
const fs = require("fs");
|
||||
|
||||
ipcMain.handle("getAppPath", (_, pathType) => {
|
||||
return app.getPath(pathType);
|
||||
});
|
||||
|
||||
ipcMain.handle("pathJoin_IPC", (_, dir, filename) => {
|
||||
return path.join(dir, filename);
|
||||
});
|
||||
|
||||
ipcMain.handle("doRPC_IPC", async (_, config, method, params) => {
|
||||
const response = await new Promise((resolve, reject) => {
|
||||
axios(config.url, {
|
||||
data: {
|
||||
jsonrpc: "2.0",
|
||||
id: "curltest",
|
||||
method: method,
|
||||
params: params,
|
||||
},
|
||||
method: "POST",
|
||||
auth: {
|
||||
username: config.username,
|
||||
password: config.password,
|
||||
},
|
||||
})
|
||||
.then((r) => resolve(r.data))
|
||||
.catch((err) => {
|
||||
const e = { ...err };
|
||||
console.log(
|
||||
`Caught error: ${e.response.toString()} - ${
|
||||
e.response ? e.response.data.toString() : ""
|
||||
}`
|
||||
);
|
||||
if (e.response && e.response.data) {
|
||||
reject(e.response.data.toString());
|
||||
} else {
|
||||
reject("NO_CONNECTION");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return response;
|
||||
});
|
||||
|
||||
ipcMain.handle("getZecPrice_IPC", async () => {
|
||||
const response = await new Promise((resolve, reject) => {
|
||||
axios("https://api.coincap.io/v2/rates/zcash", {
|
||||
method: "GET",
|
||||
})
|
||||
.then((r) => resolve(r.data))
|
||||
.catch((err) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
|
||||
return response;
|
||||
});
|
||||
|
||||
ipcMain.handle(
|
||||
"showSaveDialog_IPC",
|
||||
async (_, title, defaultPath, filters, properties) => {
|
||||
return dialog.showSaveDialog({ title, defaultPath, filters, properties });
|
||||
}
|
||||
);
|
||||
|
||||
ipcMain.handle("writefile_IPC", async (_, filename, content) => {
|
||||
return fs.promises.writeFile(filename, content);
|
||||
});
|
||||
|
||||
ipcMain.handle("readfile_IPC", async (_, filename) => {
|
||||
return JSON.parse(await fs.promises.readFile(filename));
|
||||
});
|
||||
|
||||
// Create the native browser window.
|
||||
function createWindow() {
|
||||
const mainWindow = new BrowserWindow({
|
||||
width: 2400,
|
||||
height: 800,
|
||||
// Set the path of an additional "preload" script that can be used to
|
||||
// communicate between node-land and browser-land.
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, "preload.js"),
|
||||
},
|
||||
});
|
||||
|
||||
// In production, set the initial browser path to the local bundle generated
|
||||
// by the Create React App build process.
|
||||
// In development, set it to localhost to allow live/hot-reloading.
|
||||
const appURL = app.isPackaged
|
||||
? url.format({
|
||||
pathname: path.join(__dirname, "index.html"),
|
||||
protocol: "file:",
|
||||
slashes: true,
|
||||
})
|
||||
: "http://127.0.0.1:3000";
|
||||
mainWindow.loadURL(appURL);
|
||||
|
||||
// Automatically open Chrome's DevTools in development mode.
|
||||
if (!app.isPackaged) {
|
||||
mainWindow.webContents.openDevTools();
|
||||
}
|
||||
}
|
||||
|
||||
// Setup a local proxy to adjust the paths of requested files when loading
|
||||
// them from the local production bundle (e.g.: local fonts, etc...).
|
||||
function setupLocalFilesNormalizerProxy() {
|
||||
protocol.registerHttpProtocol(
|
||||
"file",
|
||||
(request, callback) => {
|
||||
const url = request.url.substr(8);
|
||||
callback({ path: path.normalize(`${__dirname}/${url}`) });
|
||||
},
|
||||
(error) => {
|
||||
if (error) console.error("Failed to register protocol");
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// This method will be called when Electron has finished its initialization and
|
||||
// is ready to create the browser windows.
|
||||
// Some APIs can only be used after this event occurs.
|
||||
app.whenReady().then(() => {
|
||||
createWindow();
|
||||
setupLocalFilesNormalizerProxy();
|
||||
|
||||
app.on("activate", function () {
|
||||
// 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 (BrowserWindow.getAllWindows().length === 0) {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Quit when all windows are closed, except on macOS.
|
||||
// There, it's common for applications and their menu bar to stay active until
|
||||
// the user quits explicitly with Cmd + Q.
|
||||
app.on("window-all-closed", function () {
|
||||
if (process.platform !== "darwin") {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
// If your app has no need to navigate or only needs to navigate to known pages,
|
||||
// it is a good idea to limit navigation outright to that known scope,
|
||||
// disallowing any other kinds of navigation.
|
||||
const allowedNavigationDestinations = "https://my-electron-app.com";
|
||||
app.on("web-contents-created", (event, contents) => {
|
||||
contents.on("will-navigate", (event, navigationUrl) => {
|
||||
console.log("About to navigate!");
|
||||
const parsedUrl = new URL(navigationUrl);
|
||||
|
||||
if (!allowedNavigationDestinations.includes(parsedUrl.origin)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// In this file you can include the rest of your app's specific main process
|
||||
// code. You can also put them in separate files and require them here.
|
After Width: | Height: | Size: 3.8 KiB |