Switch to React UI

This commit is contained in:
Aditya Kulkarni 2020-03-05 13:32:26 -08:00
parent ede9271f58
commit 2259526aa4
336 changed files with 22744 additions and 63104 deletions

52
.dockerignore Normal file
View File

@ -0,0 +1,52 @@
# Logs
logs
*.log
# Runtime data
pids
*.pid
*.seed
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
.eslintcache
# Dependency directory
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
node_modules
# OSX
.DS_Store
# flow-typed
flow-typed/npm/*
!flow-typed/npm/module_vx.x.x.js
# App packaged
release
app/main.prod.js
app/main.prod.js.map
app/renderer.prod.js
app/renderer.prod.js.map
app/style.css
app/style.css.map
dist
dll
main.js
main.js.map
.idea
npm-debug.log.*
.*.dockerfile

12
.editorconfig Normal file
View File

@ -0,0 +1,12 @@
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false

56
.eslintignore Normal file
View File

@ -0,0 +1,56 @@
# Logs
logs
*.log
# Runtime data
pids
*.pid
*.seed
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
.eslintcache
# Dependency directory
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
node_modules
# OSX
.DS_Store
# flow-typed
flow-typed/npm/*
!flow-typed/npm/module_vx.x.x.js
# App packaged
release
app/main.prod.js
app/main.prod.js.map
app/renderer.prod.js
app/renderer.prod.js.map
app/style.css
app/style.css.map
dist
dll
main.js
main.js.map
.idea
npm-debug.log.*
__snapshots__
# Package.json
package.json
.travis.yml

10
.eslintrc.js Normal file
View File

@ -0,0 +1,10 @@
module.exports = {
extends: 'erb',
settings: {
'import/resolver': {
webpack: {
config: require.resolve('./configs/webpack.config.eslint.js')
}
}
}
}

26
.flowconfig Normal file
View File

@ -0,0 +1,26 @@
[ignore]
<PROJECT_ROOT>/app/main.prod.js
<PROJECT_ROOT>/app/main.prod.js.map
<PROJECT_ROOT>/app/dist/.*
<PROJECT_ROOT>/resources/.*
<PROJECT_ROOT>/node_modules/webpack-cli
<PROJECT_ROOT>/release/.*
<PROJECT_ROOT>/dll/.*
<PROJECT_ROOT>/release/.*
<PROJECT_ROOT>/git/.*
[include]
[libs]
[options]
esproposal.class_static_fields=enable
esproposal.class_instance_fields=enable
esproposal.export_star_as=enable
module.name_mapper.extension='css' -> '<PROJECT_ROOT>/internals/flow/CSSModule.js.flow'
module.name_mapper.extension='styl' -> '<PROJECT_ROOT>/internals/flow/CSSModule.js.flow'
module.name_mapper.extension='scss' -> '<PROJECT_ROOT>/internals/flow/CSSModule.js.flow'
module.name_mapper.extension='png' -> '<PROJECT_ROOT>/internals/flow/WebpackAsset.js.flow'
module.name_mapper.extension='jpg' -> '<PROJECT_ROOT>/internals/flow/WebpackAsset.js.flow'
suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe
suppress_comment=\\(.\\|\n\\)*\\$FlowIssue

6
.gitattributes vendored Normal file
View File

@ -0,0 +1,6 @@
* text eol=lf
*.png binary
*.jpg binary
*.jpeg binary
*.ico binary
*.icns binary

4
.github/ISSUE_TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,4 @@
<!--
Is this a bug report?
If so, go back and select the "Bug report" option or your issue WILL be closed.
--!>

6
.github/config.yml vendored Normal file
View File

@ -0,0 +1,6 @@
requiredHeaders:
- Prerequisites
- Expected Behavior
- Current Behavior
- Possible Solution
- Your Environment

17
.github/stale.yml vendored Normal file
View File

@ -0,0 +1,17 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 60
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 7
# Issues with these labels will never be considered stale
exemptLabels:
- pr
- discussion
- e2e
- enhancement
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false

84
.github/workflows/electron.yml vendored Normal file
View File

@ -0,0 +1,84 @@
name: Electron CD
on:
push:
branches:
- release
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-16.04, macOS-latest, windows-latest]
steps:
- name: Context
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT"
- uses: actions/checkout@v1
with:
fetch-depth: 1
- uses: actions-rs/toolchain@v1
with:
toolchain: 1.41.0
override: true
- name: Use Node.js 12.x
uses: actions/setup-node@v1
with:
node-version: 12.x
- name: yarn install
run: |
yarn install
- name: Build and Package@Linux
if: contains(matrix.os, 'ubuntu')
run: |
yarn package-linux
- name: Build and Package@Mac
if: contains(matrix.os, 'macos')
run: |
yarn package-mac
- name: Build and Package@Win
if: contains(matrix.os, 'windows')
run: |
yarn package-win
- name: Version@Linux@Mac
if: contains(matrix.os, 'ubuntu') || contains(matrix.os, 'macos')
run: |
./bin/printversion.sh
- name: Version@Win
if: contains(matrix.os, 'windows')
run: |
./bin/printversion.ps1
- name: Upload artifacts-deb
uses: actions/upload-artifact@v1
if: contains(matrix.os, 'ubuntu')
with:
name: ${{ matrix.os }}
path: release/Zecwallet_Lite_${{ env.VERSION }}_amd64.deb
- name: Upload artifacts-AppImage
uses: actions/upload-artifact@v1
if: contains(matrix.os, 'ubuntu')
with:
name: ${{ matrix.os }}
path: release/Zecwallet Lite-${{ env.VERSION }}.AppImage
- name: Upload artifacts-dmg
uses: actions/upload-artifact@v1
if: contains(matrix.os, 'macos')
with:
name: ${{ matrix.os }}
path: release/Zecwallet Lite-${{ env.VERSION }}.dmg
- name: Upload artifacts-zip
uses: actions/upload-artifact@v1
if: contains(matrix.os, 'windows')
with:
name: ${{ matrix.os }}
path: release/Zecwallet Lite-${{ env.VERSION }}-win.zip
- name: Upload artifacts-msi
uses: actions/upload-artifact@v1
if: contains(matrix.os, 'windows')
with:
name: ${{ matrix.os }}
path: release/Zecwallet Lite ${{ env.VERSION }}.msi

95
.gitignore vendored
View File

@ -1,42 +1,55 @@
bin/
debug/
release/
x64/
artifacts/
docs/website/public
.vscode/
res/libsodium.a
res/libsodium/libsodium*
res/libsodium.a
src/ui_*.h
*.autosave
src/precompiled.h.cpp
.qmake.stash
zecwallet
Zecwallet-Lite.app
zecwallet-lite-mingw*
zecwallet-lite.vcxproj*
zecwallet.vcxproj*
zecwallet-lite.sln
zecwallet-lite.pro.user
/Makefile
/Makefile.*
qrc_application.cpp
zec-qt-wallet_plugin_import.cpp
zecwallet_plugin_import.cpp
zec-qt-wallet_resource*
zecwallet_resource*
workspace.code-workspace
*.zip
*.tar.gz
*.xcsettings
.DS_Store
*.mak
zcashd
IDEWorkspaceChecks.plist
*.sln
# Logs
logs
*.log
# Runtime data
pids
*.pid
*.seed
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
.eslintcache
# Dependency directory
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
node_modules
zec-qt-wallet.pro.user.4.10-pre1
zecwallet-lite
zecwallet-lite_plugin_import.cpp
zecwallet-lite_resource.rc
# OSX
.DS_Store
# flow-typed
flow-typed/npm/*
!flow-typed/npm/module_vx.x.x.js
# App packaged
release
app/main.prod.js
app/main.prod.js.map
app/renderer.prod.js
app/renderer.prod.js.map
app/style.css
app/style.css.map
dist
dll
main.js
main.js.map
.idea
npm-debug.log.*
zcashd
native/artifacts.json

12
.prettierrc.json Normal file
View File

@ -0,0 +1,12 @@
{
"overrides": [
{
"files": [".prettierrc", ".babelrc", ".eslintrc", ".stylelintrc"],
"options": {
"parser": "json"
}
}
],
"singleQuote": true,
"printWidth": 120
}

3
.stylelintrc Normal file
View File

@ -0,0 +1,3 @@
{
"extends": ["stylelint-config-standard", "stylelint-config-prettier"]
}

View File

@ -1,27 +0,0 @@
matrix:
include:
# works on Precise and Trusty
- os: linux
compiler: clang
addons:
apt:
sources: ['ubuntu-toolchain-r-test', 'llvm-toolchain-precise-3.7']
packages: ['clang-3.7', 'g++-8']
before_install:
- sudo add-apt-repository ppa:beineri/opt-qt591-xenial -y
- sudo apt-get update -qq
- sudo apt-get install qt59base qt59websockets libgl1-mesa-dev
- source /opt/qt59/bin/qt59-env.sh
script:
- qmake -v
- clang++ -v
- g++-8 -v
- qmake zec-qt-wallet.pro CONFIG+=release -spec linux-clang
- make CC=clang CXX=clang++ -j2
- make distclean
- qmake zec-qt-wallet.pro CONFIG+=release -spec linux-g++
- res/libsodium/buildlibsodium.sh
- make CC=gcc-8 CXX=g++-8 -j2

8
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,8 @@
{
"recommendations": [
"dbaeumer.vscode-eslint",
"dzannotti.vscode-babel-coloring",
"EditorConfig.EditorConfig",
"flowtype.flow-for-vscode"
]
}

13
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,13 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Electron: Renderer",
"type": "chrome",
"request": "attach",
"port": 9223,
"webRoot": "${workspaceFolder}",
"timeout": 150000
}
]
}

40
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,40 @@
{
"files.associations": {
".babelrc": "jsonc",
".eslintrc": "jsonc",
".prettierrc": "jsonc",
".stylelintrc": "json",
".dockerignore": "ignore",
".eslintignore": "ignore",
".flowconfig": "ignore"
},
"javascript.validate.enable": false,
"javascript.format.enable": false,
"typescript.validate.enable": false,
"typescript.format.enable": false,
"flow.useNPMPackagedFlow": true,
"search.exclude": {
".git": true,
".eslintcache": true,
"app/dist": true,
"app/main.prod.js": true,
"app/main.prod.js.map": true,
"bower_components": true,
"dll": true,
"flow-typed": true,
"release": true,
"node_modules": true,
"npm-debug.log.*": true,
"test/**/__snapshots__": true,
"yarn.lock": true
},
"workbench.colorCustomizations": {
"activityBar.background": "#49212F",
"titleBar.activeBackground": "#662E42",
"titleBar.activeForeground": "#FCF8FA"
}
}

33
.vscode/tasks.json vendored Normal file
View File

@ -0,0 +1,33 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "dev",
"group": { "kind": "build", "isDefault": true },
"problemMatcher": []
},
{
"type": "npm",
"label": "Start Webpack Dev",
"script": "start-renderer-dev",
"options": {
"cwd": "${workspaceFolder}"
},
"isBackground": true,
"problemMatcher": {
"owner": "custom",
"pattern": {
"regexp": "____________"
},
"background": {
"activeOnStart": true,
"beginsPattern": "Compiling\\.\\.\\.$",
"endsPattern": "(Compiled successfully|Failed to compile)\\.$"
}
}
}
]
}

23
LICENSE
View File

@ -1,7 +1,22 @@
Copyright 2018 adityapk
The MIT License (MIT)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
Copyright (c) 2018-present Zecwallet
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

35
afterSignHook.js Normal file
View File

@ -0,0 +1,35 @@
/* eslint-disable camelcase */
const fs = require('fs');
const electron_notarize = require('electron-notarize');
module.exports = async function(params) {
// Only notarize the app on Mac OS only.
if (process.platform !== 'darwin') {
return;
}
// console.log('afterSign hook triggered', params);
// Same appId in electron-builder.
const appId = 'co.zecwallet.lite';
const appPath = params.artifactPaths.find(p => p.endsWith('.dmg'));
if (!fs.existsSync(appPath)) {
throw new Error(`Cannot find application at: ${appPath}`);
}
console.log(`Notarizing ${appId} found at ${appPath}`);
try {
await electron_notarize.notarize({
appBundleId: appId,
appPath,
appleId: process.env.appleId,
appleIdPassword: process.env.appleIdPassword
});
} catch (error) {
console.error(error);
}
console.log(`Done notarizing ${appId}`);
};

View File

@ -0,0 +1,4 @@
{
"mainWindowUrl": "./app.html",
"appPath": "."
}

548
app/Routes.js Normal file
View File

@ -0,0 +1,548 @@
/* eslint-disable react/jsx-props-no-spreading */
/* eslint-disable max-classes-per-file */
/* eslint-disable react/prop-types */
/* eslint-disable react/no-unused-state */
import React from 'react';
import ReactModal from 'react-modal';
import { Switch, Route } from 'react-router';
import native from '../native/index.node';
import { ErrorModal, ErrorModalData } from './components/ErrorModal';
import cstyles from './components/Common.css';
import routes from './constants/routes.json';
import App from './containers/App';
import Dashboard from './components/Dashboard';
import Send from './components/Send';
import Receive from './components/Receive';
import LoadingScreen from './components/LoadingScreen';
import AppState, {
AddressBalance,
TotalBalance,
Transaction,
SendPageState,
ToAddr,
RPCConfig,
Info,
ReceivePageState,
AddressBookEntry,
PasswordState
} from './components/AppState';
import RPC from './rpc';
import Utils from './utils/utils';
import Zcashd from './components/Zcashd';
import AddressBook from './components/Addressbook';
import AddressbookImpl from './utils/AddressbookImpl';
import Sidebar from './components/Sidebar';
import Transactions from './components/Transactions';
import PasswordModal from './components/PasswordModal';
import CompanionAppListener from './companion';
import WormholeConnection from './components/WormholeConnection';
type Props = {};
export default class RouteApp extends React.Component<Props, AppState> {
rpc: RPC;
companionAppListener: CompanionAppListener;
constructor(props) {
super(props);
this.state = {
totalBalance: new TotalBalance(),
addressesWithBalance: [],
addressPrivateKeys: {},
addresses: [],
addressBook: [],
transactions: null,
sendPageState: new SendPageState(),
receivePageState: new ReceivePageState(),
rpcConfig: new RPCConfig(),
info: new Info(),
rescanning: false,
location: null,
errorModalData: new ErrorModalData(),
passwordState: new PasswordState(),
connectedCompanionApp: null
};
// Create the initial ToAddr box
// eslint-disable-next-line react/destructuring-assignment
this.state.sendPageState.toaddrs = [new ToAddr(Utils.getNextToAddrID())];
// Set the Modal's app element
ReactModal.setAppElement('#root');
console.log(native.litelib_wallet_exists('main'));
}
componentDidMount() {
if (!this.rpc) {
this.rpc = new RPC(
this.setTotalBalance,
this.setAddressesWithBalances,
this.setTransactionList,
this.setAllAddresses,
this.setInfo,
this.setZecPrice
);
}
// Read the address book
(async () => {
const addressBook = await AddressbookImpl.readAddressBook();
if (addressBook) {
this.setState({ addressBook });
}
})();
// Setup the websocket for the companion app
this.companionAppListener = new CompanionAppListener(
this.getFullState,
this.sendTransaction,
this.updateConnectedCompanionApp
);
this.companionAppListener.setUp();
}
componentWillUnmount() {}
getFullState = (): AppState => {
return this.state;
};
openErrorModal = (title: string, body: string) => {
const errorModalData = new ErrorModalData();
errorModalData.modalIsOpen = true;
errorModalData.title = title;
errorModalData.body = body;
this.setState({ errorModalData });
};
closeErrorModal = () => {
const errorModalData = new ErrorModalData();
errorModalData.modalIsOpen = false;
this.setState({ errorModalData });
};
openPassword = (
confirmNeeded: boolean,
passwordCallback: string => void,
closeCallback: () => void,
helpText: string
) => {
const passwordState = new PasswordState();
passwordState.showPassword = true;
passwordState.confirmNeeded = confirmNeeded;
passwordState.helpText = helpText;
// Set the callbacks, but before calling them back, we close the modals
passwordState.passwordCallback = (password: string) => {
this.setState({ passwordState: new PasswordState() });
// Call the callback after a bit, so as to give time to the modal to close
setTimeout(() => passwordCallback(password), 10);
};
passwordState.closeCallback = () => {
this.setState({ passwordState: new PasswordState() });
// Call the callback after a bit, so as to give time to the modal to close
setTimeout(() => closeCallback(), 10);
};
this.setState({ passwordState });
};
// This will:
// 1. Check if the wallet is encrypted and locked
// 2. If it is, open the password dialog
// 3. Attempt to unlock wallet.
// a. If unlock suceeds, do the callback
// b. If the unlock fails, show an error
// 4. If wallet is not encrypted or already unlocked, just call the successcallback.
openPasswordAndUnlockIfNeeded = (successCallback: () => void) => {
// Check if it is locked
const { info } = this.state;
if (info.encrypted && info.locked) {
this.openPassword(
false,
(password: string) => {
(async () => {
const success = await this.unlockWallet(password);
if (success) {
// If the unlock succeeded, do the submit
successCallback();
} else {
this.openErrorModal('Wallet unlock failed', 'Could not unlock the wallet with the password.');
}
})();
},
// Close callback is a no-op
() => {}
);
} else {
successCallback();
}
};
unlockWallet = async (password: string): boolean => {
const success = await this.rpc.unlockWallet(password);
return success;
};
lockWallet = async (): boolean => {
const success = await this.rpc.lockWallet();
return success;
};
encryptWallet = async (password): boolean => {
const success = await this.rpc.encryptWallet(password);
return success;
};
decryptWallet = async (password): boolean => {
const success = await this.rpc.decryptWallet(password);
return success;
};
setInfo = (info: Info) => {
this.setState({ info });
};
setTotalBalance = (totalBalance: TotalBalance) => {
this.setState({ totalBalance });
};
setAddressesWithBalances = (addressesWithBalance: AddressBalance[]) => {
this.setState({ addressesWithBalance });
const { sendPageState } = this.state;
// If there is no 'from' address, we'll set a default one
if (!sendPageState.fromaddr) {
// Find a z-address with the highest balance
const defaultAB = addressesWithBalance
.filter(ab => Utils.isSapling(ab.address))
.reduce((prev, ab) => {
// We'll start with a sapling address
if (prev == null) {
return ab;
}
// Find the sapling address with the highest balance
if (prev.balance < ab.balance) {
return ab;
}
return prev;
}, null);
if (defaultAB) {
const newSendPageState = new SendPageState();
newSendPageState.fromaddr = defaultAB.address;
newSendPageState.toaddrs = sendPageState.toaddrs;
this.setState({ sendPageState: newSendPageState });
}
}
};
setTransactionList = (transactions: Transaction[]) => {
this.setState({ transactions });
};
setAllAddresses = (addresses: string[]) => {
this.setState({ addresses });
};
setSendPageState = (sendPageState: SendPageState) => {
this.setState({ sendPageState });
};
setSendTo = (address: string, amount: number | null, memo: string | null) => {
// Clear the existing send page state and set up the new one
const { sendPageState } = this.state;
const newSendPageState = new SendPageState();
newSendPageState.fromaddr = sendPageState.fromaddr;
const to = new ToAddr(Utils.getNextToAddrID());
if (address) {
to.to = address;
}
if (amount) {
to.amount = amount;
}
if (memo) {
to.memo = memo;
}
newSendPageState.toaddrs = [to];
this.setState({ sendPageState: newSendPageState });
};
setRPCConfig = (rpcConfig: RPCConfig) => {
this.setState({ rpcConfig });
console.log(rpcConfig);
this.rpc.configure(rpcConfig);
};
setZecPrice = (price: number | null) => {
console.log(`Price = ${price}`);
const { info } = this.state;
const newInfo = new Info();
Object.assign(newInfo, info);
newInfo.zecPrice = price;
this.setState({ info: newInfo });
};
setRescanning = (rescanning: boolean) => {
this.setState({ rescanning });
};
setInfo = (newInfo: Info) => {
// If the price is not set in this object, copy it over from the current object
const { info } = this.state;
if (!newInfo.zecPrice) {
// eslint-disable-next-line no-param-reassign
newInfo.zecPrice = info.zecPrice;
}
this.setState({ info: newInfo });
};
sendTransaction = (sendJson: []): string => {
try {
const txid = this.rpc.sendTransaction(sendJson);
return txid;
} catch (err) {
console.log('route sendtx error', err);
}
};
// Get a single private key for this address, and return it as a string.
// Wallet needs to be unlocked
getPrivKeyAsString = (address: string): string => {
const pk = RPC.getPrivKeyAsString(address);
return pk;
};
// Getter methods, which are called by the components to update the state
fetchAndSetSinglePrivKey = async (address: string) => {
this.openPasswordAndUnlockIfNeeded(async () => {
const key = await RPC.getPrivKeyAsString(address);
const addressPrivateKeys = {};
addressPrivateKeys[address] = key;
this.setState({ addressPrivateKeys });
});
};
addAddressBookEntry = (label: string, address: string) => {
// Add an entry into the address book
const { addressBook } = this.state;
const newAddressBook = addressBook.concat(new AddressBookEntry(label, address));
// Write to disk. This method is async
AddressbookImpl.writeAddressBook(newAddressBook);
this.setState({ addressBook: newAddressBook });
};
removeAddressBookEntry = (label: string) => {
const { addressBook } = this.state;
const newAddressBook = addressBook.filter(i => i.label !== label);
// Write to disk. This method is async
AddressbookImpl.writeAddressBook(newAddressBook);
this.setState({ addressBook: newAddressBook });
};
createNewAddress = async (zaddress: boolean) => {
// Create a new address
const newaddress = RPC.createNewAddress(zaddress);
console.log(`Created new Address ${newaddress}`);
// And then fetch the list of addresses again to refresh (totalBalance gets all addresses)
this.rpc.fetchTotalBalance();
const { receivePageState } = this.state;
const newRerenderKey = receivePageState.rerenderKey + 1;
const newReceivePageState = new ReceivePageState();
newReceivePageState.newAddress = newaddress;
newReceivePageState.rerenderKey = newRerenderKey;
this.setState({ receivePageState: newReceivePageState });
};
updateConnectedCompanionApp = (connectedCompanionApp: ConnectedCompanionApp | null) => {
this.setState({ connectedCompanionApp });
};
doRefresh = () => {
this.rpc.refresh();
};
render() {
const {
totalBalance,
transactions,
addressesWithBalance,
addressPrivateKeys,
addresses,
addressBook,
sendPageState,
receivePageState,
rpcConfig,
info,
rescanning,
errorModalData,
passwordState,
connectedCompanionApp
} = this.state;
const standardProps = {
openErrorModal: this.openErrorModal,
closeErrorModal: this.closeErrorModal,
setSendTo: this.setSendTo,
info,
openPasswordAndUnlockIfNeeded: this.openPasswordAndUnlockIfNeeded
};
return (
<App>
<ErrorModal
title={errorModalData.title}
body={errorModalData.body}
modalIsOpen={errorModalData.modalIsOpen}
closeModal={this.closeErrorModal}
/>
<PasswordModal
modalIsOpen={passwordState.showPassword}
confirmNeeded={passwordState.confirmNeeded}
passwordCallback={passwordState.passwordCallback}
closeCallback={passwordState.closeCallback}
helpText={passwordState.helpText}
/>
<div style={{ overflow: 'hidden' }}>
{info && info.version && (
<div className={cstyles.sidebarcontainer}>
<Sidebar
info={info}
setInfo={this.setInfo}
setSendTo={this.setSendTo}
setRescanning={this.setRescanning}
getPrivKeyAsString={this.getPrivKeyAsString}
addresses={addresses}
lockWallet={this.lockWallet}
encryptWallet={this.encryptWallet}
decryptWallet={this.decryptWallet}
openPassword={this.openPassword}
{...standardProps}
/>
</div>
)}
<div className={cstyles.contentcontainer}>
<Switch>
<Route
path={routes.SEND}
render={() => (
<Send
addresses={addresses}
sendTransaction={this.sendTransaction}
sendPageState={sendPageState}
setSendPageState={this.setSendPageState}
totalBalance={totalBalance}
addressBook={addressBook}
{...standardProps}
/>
)}
/>
<Route
path={routes.RECEIVE}
render={() => (
<Receive
rerenderKey={receivePageState.rerenderKey}
addresses={addresses}
addressesWithBalance={addressesWithBalance}
addressPrivateKeys={addressPrivateKeys}
receivePageState={receivePageState}
addressBook={addressBook}
{...standardProps}
fetchAndSetSinglePrivKey={this.fetchAndSetSinglePrivKey}
createNewAddress={this.createNewAddress}
/>
)}
/>
<Route
path={routes.ADDRESSBOOK}
render={() => (
<AddressBook
addressBook={addressBook}
addAddressBookEntry={this.addAddressBookEntry}
removeAddressBookEntry={this.removeAddressBookEntry}
{...standardProps}
/>
)}
/>
<Route
path={routes.DASHBOARD}
// eslint-disable-next-line react/jsx-props-no-spreading
render={() => (
<Dashboard totalBalance={totalBalance} info={info} addressesWithBalance={addressesWithBalance} />
)}
/>
<Route
path={routes.TRANSACTIONS}
render={() => (
<Transactions
transactions={transactions}
info={info}
addressBook={addressBook}
setSendTo={this.setSendTo}
/>
)}
/>
<Route
path={routes.ZCASHD}
render={() => <Zcashd info={info} rpcConfig={rpcConfig} refresh={this.doRefresh} />}
/>
<Route
path={routes.CONNECTMOBILE}
render={() => (
<WormholeConnection
companionAppListener={this.companionAppListener}
connectedCompanionApp={connectedCompanionApp}
/>
)}
/>
<Route
path={routes.LOADING}
render={() => (
<LoadingScreen
setRPCConfig={this.setRPCConfig}
rescanning={rescanning}
setRescanning={this.setRescanning}
setInfo={this.setInfo}
/>
)}
/>
</Switch>
</div>
</div>
</App>
);
}
}

144
app/app.global.css Normal file
View File

@ -0,0 +1,144 @@
/*
* @NOTE: Prepend a `~` to css file paths that are in your node_modules
* See https://github.com/webpack-contrib/sass-loader#imports
*/
@import '~@fortawesome/fontawesome-free/css/all.css';
@import '~typeface-roboto/index.css';
html {
overflow: hidden;
}
body {
position: relative;
color: white;
height: 100vh;
background-color: #212124;
font-family: Roboto, Arial, Helvetica, Helvetica Neue, serif;
}
::-webkit-scrollbar {
width: 0.5em;
}
::-webkit-scrollbar-track {
box-shadow: inset 0 0 6px grey;
background-color: rgb(1, 1, 1);
}
::-webkit-scrollbar-thumb {
background-color: white;
outline: 1px solid slategrey;
}
h2 {
margin: 0;
font-size: 2.25rem;
font-weight: bold;
letter-spacing: -0.025em;
color: #fff;
}
p {
font-size: 12px;
}
li {
list-style: none;
}
a {
color: white;
text-decoration: none;
}
input[disabled] {
color: grey;
}
.react-tabs {
-webkit-tap-highlight-color: transparent;
}
.react-tabs__tab-list {
border-bottom: 1px solid #aaa;
margin: 0 0 10px;
padding: 0;
}
.react-tabs__tab {
display: inline-block;
border: 1px solid transparent;
border-bottom: none;
bottom: -1px;
position: relative;
list-style: none;
padding: 6px 12px;
cursor: pointer;
font-size: 16px;
}
.react-tabs__tab--selected {
background: #c3921f;
color: black;
border-radius: 5px 5px 0 0;
}
.react-tabs__tab--disabled {
color: grey;
cursor: default;
}
.react-tabs__tab:focus {
box-shadow: 0 0 5px hsl(208, 99%, 50%);
border-color: hsl(208, 99%, 50%);
outline: none;
}
.react-tabs__tab:focus::after {
content: '';
position: absolute;
height: 5px;
left: -4px;
right: -4px;
bottom: -5px;
background: #fff;
}
.react-tabs__tab-panel {
display: none;
}
.react-tabs__tab-panel--selected {
display: block;
}
img,
img::after,
img::before {
-webkit-user-select: none;
-webkit-user-drag: none;
-webkit-app-region: no-drag;
cursor: default;
}
a,
a::after,
a::before {
-webkit-user-select: none;
-webkit-user-drag: none;
-webkit-app-region: no-drag;
cursor: default;
}
a:hover {
opacity: 0.8;
text-decoration: none;
cursor: pointer;
}
input[type='number']::-webkit-inner-spin-button,
input[type='number']::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}

48
app/app.html Normal file
View File

@ -0,0 +1,48 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Zecwallet Lite</title>
<!-- Allow unsafe only for inline script tags, no external content-->
<script nonce="1u1na">
if (process.env.NODE_ENV === 'production') {
document.write(
`<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline'; style-src 'self'; " >`
);
}
(() => {
if (!process.env.START_HOT) {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = './dist/style.css';
// HACK: Writing the script path should be done with webpack
document.getElementsByTagName('head')[0].appendChild(link);
}
})();
</script>
</head>
<body>
<div id="root"></div>
<script nonce="8SHwb">
{
const scripts = [];
// Dynamically insert the DLL script in development env in the
// renderer process
if (process.env.NODE_ENV === 'development') {
scripts.push('../dll/renderer.dev.dll.js');
}
// Dynamically insert the bundled app script in the renderer process
const port = process.env.PORT || 1212;
scripts.push(
process.env.START_HOT ? `http://localhost:${port}/dist/renderer.dev.js` : './dist/renderer.prod.js'
);
document.write(scripts.map(script => `<script defer src="${script}"><\/script>`).join(''));
}
</script>
</body>
</html>

BIN
app/app.icns Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 747 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
app/assets/img/logobig.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

BIN
app/assets/img/logobig.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

499
app/companion.js Normal file
View File

@ -0,0 +1,499 @@
/* eslint-disable flowtype/no-weak-types */
/* eslint-disable max-classes-per-file */
/* eslint-disable class-methods-use-this */
import _sodium from 'libsodium-wrappers-sumo';
import Store from 'electron-store';
import WebSocket from 'ws';
import AppState, { ConnectedCompanionApp } from './components/AppState';
import Utils from './utils/utils';
// Wormhole code is sha256(sha256(secret_key))
function getWormholeCode(keyHex: string, sodium: any): string {
const key = sodium.from_hex(keyHex);
const pass1 = sodium.crypto_hash_sha256(key);
const pass2 = sodium.to_hex(sodium.crypto_hash_sha256(pass1));
return pass2;
}
// A class that connects to wormhole given a secret key
class WormholeClient {
keyHex: string;
wormholeCode: string;
sodium: any;
wss: WebSocket = null;
listner: CompanionAppListener = null;
keepAliveTimerID: TimerID = null;
constructor(keyHex: string, sodium: any, listner: CompanionAppListener) {
this.keyHex = keyHex;
this.sodium = sodium;
this.listner = listner;
this.wormholeCode = getWormholeCode(keyHex, this.sodium);
this.connect();
}
connect() {
this.wss = new WebSocket('wss://wormhole.zecqtwallet.com:443');
this.wss.on('open', () => {
// On open, register ourself
const reg = { register: getWormholeCode(this.keyHex, this.sodium) };
// No encryption for the register call
this.wss.send(JSON.stringify(reg));
// Now, do a ping every 4 minutes to keep the connection alive.
this.keepAliveTimerID = setInterval(() => {
const ping = { ping: 'ping' };
this.wss.send(JSON.stringify(ping));
}, 4 * 60 * 1000);
});
this.wss.on('message', data => {
this.listner.processIncoming(data, this.keyHex, this.wss);
});
this.wss.on('close', (code, reason) => {
console.log('Socket closed for ', this.keyHex, code, reason);
});
this.wss.on('error', (ws, err) => {
console.log('ws error', err);
});
}
getKeyHex(): string {
return this.keyHex;
}
close() {
if (this.keepAliveTimerID) {
clearInterval(this.keepAliveTimerID);
}
// Close the websocket.
if (this.wss) {
this.wss.close();
}
}
}
// The singleton Companion App listener, that can spawn a wormhole server
// or (multiple) wormhole clients
export default class CompanionAppListener {
sodium = null;
fnGetState: () => AppState = null;
fnSendTransaction: ([]) => string = null;
fnUpdateConnectedClient: (string, number) => void = null;
permWormholeClient: WormholeClient = null;
tmpWormholeClient: WormholeClient = null;
constructor(
fnGetSate: () => AppState,
fnSendTransaction: ([]) => string,
fnUpdateConnectedClient: (string, number) => void
) {
this.fnGetState = fnGetSate;
this.fnSendTransaction = fnSendTransaction;
this.fnUpdateConnectedClient = fnUpdateConnectedClient;
}
async setUp() {
await _sodium.ready;
this.sodium = _sodium;
// Create a new wormhole listner
const permKeyHex = this.getEncKey();
if (permKeyHex) {
this.permWormholeClient = new WormholeClient(permKeyHex, this.sodium, this);
}
// At startup, set the last client name/time by loading it
const store = new Store();
const name = store.get('companion/name');
const lastSeen = store.get('companion/lastseen');
if (name && lastSeen) {
const o = new ConnectedCompanionApp();
o.name = name;
o.lastSeen = lastSeen;
this.fnUpdateConnectedClient(o);
}
}
createTmpClient(keyHex: string) {
if (this.tmpWormholeClient) {
this.tmpWormholeClient.close();
}
this.tmpWormholeClient = new WormholeClient(keyHex, this.sodium, this);
}
closeTmpClient() {
if (this.tmpWormholeClient) {
this.tmpWormholeClient.close();
this.tmpWormholeClient = null;
}
}
replacePermClientWithTmp() {
if (this.permWormholeClient) {
this.permWormholeClient.close();
}
// Replace the stored code with the new one
this.permWormholeClient = this.tmpWormholeClient;
this.tmpWormholeClient = null;
this.setEncKey(this.permWormholeClient.getKeyHex());
// Reset local nonce
const store = new Store();
store.delete('companion/localnonce');
}
processIncoming(data: string, keyHex: string, ws: Websocket) {
const dataJson = JSON.parse(data);
// If the wormhole sends some messages, we ignore them
if ('error' in dataJson) {
console.log('Incoming data contains an error message', data);
return;
}
// If the message is a ping, just ignore it
if ('ping' in dataJson) {
return;
}
// Then, check if the message is encrpted
if (!('nonce' in dataJson)) {
const err = { error: 'Encryption error', to: getWormholeCode(keyHex, this.sodium) };
ws.send(JSON.stringify(err));
return;
}
let cmd;
// If decryption passes and this is a tmp wormhole client, then set it as the permanant client
if (this.tmpWormholeClient && keyHex === this.tmpWormholeClient.getKeyHex()) {
const { decrypted, nonce } = this.decryptIncoming(data, keyHex, false);
if (!decrypted) {
console.log('Decryption failed');
const err = { error: 'Encryption error', to: getWormholeCode(keyHex, this.sodium) };
ws.send(JSON.stringify(err));
return;
}
cmd = JSON.parse(decrypted);
// Replace the permanant client
this.replacePermClientWithTmp();
this.updateRemoteNonce(nonce);
} else {
const { decrypted, nonce } = this.decryptIncoming(data, keyHex, true);
if (!decrypted) {
const err = { error: 'Encryption error', to: getWormholeCode(keyHex, this.sodium) };
ws.send(JSON.stringify(err));
console.log('Decryption failed');
return;
}
cmd = JSON.parse(decrypted);
this.updateRemoteNonce(nonce);
}
if (cmd.command === 'getInfo') {
const response = this.doGetInfo(cmd);
ws.send(this.encryptOutgoing(response, keyHex));
} else if (cmd.command === 'getTransactions') {
const response = this.doGetTransactions();
ws.send(this.encryptOutgoing(response, keyHex));
} else if (cmd.command === 'sendTx') {
const response = this.doSendTransaction(cmd, ws);
ws.send(this.encryptOutgoing(response, keyHex));
}
}
// Generate a new secret key
genNewKeyHex(): string {
const keyHex = this.sodium.to_hex(this.sodium.crypto_secretbox_keygen());
return keyHex;
}
getEncKey(): string {
// Get the nonce. Increment and store the nonce for next use
const store = new Store();
const keyHex = store.get('companion/key');
return keyHex;
}
setEncKey(keyHex: string) {
// Get the nonce. Increment and store the nonce for next use
const store = new Store();
store.set('companion/key', keyHex);
}
saveLastClientName(name: string) {
// Save the last client name
const store = new Store();
store.set('companion/name', name);
if (name) {
const now = Date.now();
store.set('companion/lastseen', now);
const o = new ConnectedCompanionApp();
o.name = name;
o.lastSeen = now;
this.fnUpdateConnectedClient(o);
} else {
this.fnUpdateConnectedClient(null);
}
}
disconnectLastClient() {
// Remove the permanant connection
if (this.permWormholeClient) {
this.permWormholeClient.close();
}
this.saveLastClientName(null);
this.setEncKey(null);
}
getRemoteNonce(): string {
const store = new Store();
const nonceHex = store.get('companion/remotenonce');
return nonceHex;
}
updateRemoteNonce(nonce: string) {
if (nonce) {
const store = new Store();
store.set('companion/remotenonce', nonce);
}
}
getLocalNonce(): string {
// Get the nonce. Increment and store the nonce for next use
const store = new Store();
const nonceHex = store.get('companion/localnonce', `01${'00'.repeat(this.sodium.crypto_secretbox_NONCEBYTES - 1)}`);
// Increment nonce
const newNonce = this.sodium.from_hex(nonceHex);
this.sodium.increment(newNonce);
this.sodium.increment(newNonce);
store.set('companion/localnonce', this.sodium.to_hex(newNonce));
return nonceHex;
}
encryptOutgoing(str: string, keyHex: string): string {
if (!keyHex) {
console.log('No secret key');
throw Error('No Secret Key');
}
const nonceHex = this.getLocalNonce();
const nonce = this.sodium.from_hex(nonceHex);
const key = this.sodium.from_hex(keyHex);
const encrypted = this.sodium.crypto_secretbox_easy(str, nonce, key);
const encryptedHex = this.sodium.to_hex(encrypted);
const resp = {
nonce: this.sodium.to_hex(nonce),
payload: encryptedHex,
to: getWormholeCode(keyHex, this.sodium)
};
return JSON.stringify(resp);
}
decryptIncoming(msg: string, keyHex: string, checkNonce: boolean): any {
const msgJson = JSON.parse(msg);
console.log('trying to decrypt', msgJson);
if (!keyHex) {
console.log('No secret key');
throw Error('No Secret Key');
}
const key = this.sodium.from_hex(keyHex);
const nonce = this.sodium.from_hex(msgJson.nonce);
if (checkNonce) {
const prevNonce = this.sodium.from_hex(this.getRemoteNonce());
if (prevNonce && this.sodium.compare(prevNonce, nonce) >= 0) {
return { decrypted: null };
}
}
const cipherText = this.sodium.from_hex(msgJson.payload);
const decrypted = this.sodium.to_string(this.sodium.crypto_secretbox_open_easy(cipherText, nonce, key));
return { decrypted, nonce: msgJson.nonce };
}
doGetInfo(cmd: any): string {
const appState = this.fnGetState();
if (cmd && cmd.name) {
this.saveLastClientName(cmd.name);
}
const saplingAddress = appState.addresses.find(a => Utils.isSapling(a));
const tAddress = appState.addresses.find(a => Utils.isTransparent(a));
const balance = parseFloat(appState.totalBalance.total);
const maxspendable = parseFloat(appState.totalBalance.total);
const maxzspendable = parseFloat(appState.totalBalance.private);
const tokenName = appState.info.currencyName;
const zecprice = parseFloat(appState.info.zecPrice);
const resp = {
version: 1.0,
command: 'getInfo',
saplingAddress,
tAddress,
balance,
maxspendable,
maxzspendable,
tokenName,
zecprice,
serverversion: '0.9.2'
};
return JSON.stringify(resp);
}
doGetTransactions(): string {
const appState = this.fnGetState();
let txlist = [];
if (appState.transactions) {
// Get only the last 20 txns
txlist = appState.transactions.slice(0, 20).map(t => {
let memo = t.detailedTxns && t.detailedTxns.length > 0 ? t.detailedTxns[0].memo : '';
if (memo) {
memo = memo.trimRight();
} else {
memo = '';
}
const txResp = {
type: t.type,
datetime: t.time,
amount: t.amount.toFixed(8),
txid: t.txid,
address: t.address,
memo,
confirmations: t.confirmations
};
return txResp;
});
}
const resp = {
version: 1.0,
command: 'getTransactions',
transactions: txlist
};
return JSON.stringify(resp);
}
doSendTransaction(cmd: any, ws: WebSocket): string {
// "command":"sendTx","tx":{"amount":"0.00019927","to":"zs1pzr7ee53jwa3h3yvzdjf7meruujq84w5rsr5kuvye9qg552kdyz5cs5ywy5hxkxcfvy9wln94p6","memo":""}}
const inpTx = cmd.tx;
const appState = this.fnGetState();
// eslint-disable-next-line radix
const sendingAmount = parseInt((parseFloat(inpTx.amount) * 10 ** 8).toFixed(0));
const buildError = (reason: string): string => {
const resp = {
errorCode: -1,
errorMessage: `Couldn't send Tx:${reason}`
};
// console.log('sendtx error', resp);
return JSON.stringify(resp);
};
// First, find an address that can send the correct amount.
const fromAddress = appState.addressesWithBalance.find(ab => ab.balance > sendingAmount);
if (!fromAddress) {
return buildError(`No address with sufficient balance to send ${sendingAmount}`);
}
const memo = !inpTx.memo || inpTx.memo.trim() === '' ? null : inpTx.memo;
// Build a sendJSON object
const sendJSON = [];
if (memo) {
sendJSON.push({ address: inpTx.to, amount: sendingAmount, memo });
} else {
sendJSON.push({ address: inpTx.to, amount: sendingAmount });
}
console.log('sendjson is', sendJSON);
let resp;
try {
const txid = this.fnSendTransaction(sendJSON);
// After the transaction is submitted, we return an intermediate success.
resp = {
version: 1.0,
command: 'sendTx',
result: 'success'
};
ws.send(this.encryptOutgoing(JSON.stringify(resp)));
// And then another one when the Tx was submitted successfully. For lightclient, this is the same,
// so we end up sending 2 responses back to back
resp = {
version: 1.0,
command: 'sendTxSubmitted',
txid
};
} catch (err) {
resp = {
version: 1.0,
command: 'sendTxFailed',
err
};
}
return JSON.stringify(resp);
}
}

View File

@ -0,0 +1,38 @@
.addressbookcontainer {
display: flex;
flex-direction: column;
margin-left: 16px;
margin-right: 16px;
}
.addressbooklist {
margin-top: 24px;
}
.addressbookentry {
padding-left: 8px;
padding-right: 8px;
}
.addressbookentry:hover {
background-color: #000;
color: #c3921f;
cursor: pointer;
border: solid 1px grey;
padding: 8px;
margin-bottom: 8px;
}
.addressbookentrybuttons {
display: flex;
justify-content: center;
margin-bottom: 8px;
}
.addressbookentry .addressbookentrybuttons {
display: none;
}
.addressbookentry:hover .addressbookentrybuttons {
display: flex;
}

View File

@ -0,0 +1,200 @@
/* eslint-disable react/prop-types */
import React, { Component } from 'react';
import { withRouter } from 'react-router-dom';
import {
AccordionItemButton,
AccordionItem,
AccordionItemHeading,
AccordionItemPanel,
Accordion
} from 'react-accessible-accordion';
import styles from './Addressbook.css';
import cstyles from './Common.css';
import { AddressBookEntry } from './AppState';
import ScrollPane from './ScrollPane';
import Utils from '../utils/utils';
import routes from '../constants/routes.json';
// Internal because we're using withRouter just below
const AddressBookItemInteral = ({ item, removeAddressBookEntry, setSendTo, history }) => {
return (
<AccordionItem key={item.label} className={[cstyles.well, cstyles.margintopsmall].join(' ')} uuid={item.label}>
<AccordionItemHeading>
<AccordionItemButton className={cstyles.accordionHeader}>
<div className={[cstyles.flexspacebetween].join(' ')}>
<div>{item.label}</div>
<div>{item.address}</div>
</div>
</AccordionItemButton>
</AccordionItemHeading>
<AccordionItemPanel>
<div className={[cstyles.well, styles.addressbookentrybuttons].join(' ')}>
<button
type="button"
className={cstyles.primarybutton}
onClick={() => {
setSendTo(item.address, null, null);
history.push(routes.SEND);
}}
>
Send To
</button>
<button type="button" className={cstyles.primarybutton} onClick={() => removeAddressBookEntry(item.label)}>
Delete
</button>
</div>
</AccordionItemPanel>
</AccordionItem>
);
};
const AddressBookItem = withRouter(AddressBookItemInteral);
type Props = {
addressBook: AddressBookEntry[],
addAddressBookEntry: (label: string, address: string) => void,
removeAddressBookEntry: (label: string) => void,
setSendTo: (address: string, amount: number | null, memo: string | null) => void
};
type State = {
currentLabel: string,
currentAddress: string,
addButtonEnabled: boolean
};
export default class AddressBook extends Component<Props, State> {
constructor(props) {
super(props);
this.state = { currentLabel: '', currentAddress: '', addButtonEnabled: false };
}
updateLabel = (currentLabel: string) => {
// Don't update the field if it is longer than 20 chars
if (currentLabel.length > 20) return;
const { currentAddress } = this.state;
this.setState({ currentLabel });
const { labelError, addressIsValid } = this.validate(currentLabel, currentAddress);
this.setAddButtonEnabled(!labelError && addressIsValid && currentLabel !== '' && currentAddress !== '');
};
updateAddress = (currentAddress: string) => {
const { currentLabel } = this.state;
this.setState({ currentAddress });
const { labelError, addressIsValid } = this.validate(currentLabel, currentAddress);
this.setAddButtonEnabled(!labelError && addressIsValid && currentLabel !== '' && currentAddress !== '');
};
addButtonClicked = () => {
const { addAddressBookEntry } = this.props;
const { currentLabel, currentAddress } = this.state;
addAddressBookEntry(currentLabel, currentAddress);
this.setState({ currentLabel: '', currentAddress: '' });
};
setAddButtonEnabled = (addButtonEnabled: boolean) => {
this.setState({ addButtonEnabled });
};
validate = (currentLabel, currentAddress) => {
const { addressBook } = this.props;
let labelError = addressBook.find(i => i.label === currentLabel) ? 'Duplicate Label' : null;
labelError = currentLabel.length > 12 ? 'Label is too long' : labelError;
const addressIsValid =
currentAddress === '' || Utils.isZaddr(currentAddress) || Utils.isTransparent(currentAddress);
return { labelError, addressIsValid };
};
render() {
const { addressBook, removeAddressBookEntry, setSendTo } = this.props;
const { currentLabel, currentAddress, addButtonEnabled } = this.state;
const { labelError, addressIsValid } = this.validate(currentLabel, currentAddress);
return (
<div>
<div className={[cstyles.xlarge, cstyles.padall, cstyles.center].join(' ')}>Address Book</div>
<div className={styles.addressbookcontainer}>
<div className={[cstyles.well].join(' ')}>
<div className={[cstyles.flexspacebetween].join(' ')}>
<div className={cstyles.sublight}>Label</div>
<div className={cstyles.validationerror}>
{!labelError ? (
<i className={[cstyles.green, 'fas', 'fa-check'].join(' ')} />
) : (
<span className={cstyles.red}>{labelError}</span>
)}
</div>
</div>
<input
type="text"
value={currentLabel}
className={[cstyles.inputbox, cstyles.margintopsmall].join(' ')}
onChange={e => this.updateLabel(e.target.value)}
/>
<div className={cstyles.margintoplarge} />
<div className={[cstyles.flexspacebetween].join(' ')}>
<div className={cstyles.sublight}>Address</div>
<div className={cstyles.validationerror}>
{addressIsValid ? (
<i className={[cstyles.green, 'fas', 'fa-check'].join(' ')} />
) : (
<span className={cstyles.red}>Invalid Address</span>
)}
</div>
</div>
<input
type="text"
value={currentAddress}
className={[cstyles.inputbox, cstyles.margintopsmall].join(' ')}
onChange={e => this.updateAddress(e.target.value)}
/>
<div className={cstyles.margintoplarge} />
<button
type="button"
className={cstyles.primarybutton}
disabled={!addButtonEnabled}
onClick={this.addButtonClicked}
>
Add
</button>
</div>
<ScrollPane offsetHeight={300}>
<div className={styles.addressbooklist}>
<div className={[cstyles.flexspacebetween, cstyles.tableheader, cstyles.sublight].join(' ')}>
<div>Label</div>
<div>Address</div>
</div>
{addressBook && (
<Accordion>
{addressBook.map(item => (
<AddressBookItem
key={item.label}
item={item}
removeAddressBookEntry={removeAddressBookEntry}
setSendTo={setSendTo}
/>
))}
</Accordion>
)}
</div>
</ScrollPane>
</div>
</div>
);
}
}

209
app/components/AppState.js Normal file
View File

@ -0,0 +1,209 @@
/* eslint-disable max-classes-per-file */
export class TotalBalance {
// Total t address, confirmed and spendable
transparent: number;
// Total private, confirmed + unconfirmed
private: number;
// Total private, confirmed funds that are spendable
verifiedPrivate: number;
// Total unconfirmed + spendable
total: number;
}
export class AddressBalance {
address: string;
balance: number;
containsPending: boolean;
constructor(address: string, balance: number) {
this.address = address;
this.balance = balance;
this.containsPending = false;
}
}
export class AddressBookEntry {
label: string;
address: string;
constructor(label: string, address: string) {
this.label = label;
this.address = address;
}
}
export class TxDetail {
address: string;
amount: string;
memo: string | null;
}
// List of transactions. TODO: Handle memos, multiple addresses etc...
export class Transaction {
type: string;
address: string;
amount: number;
confirmations: number;
txid: string;
time: number;
detailedTxns: TxDetail[];
}
export class ToAddr {
id: number;
to: string;
amount: number;
memo: string;
constructor(id: number) {
this.id = id;
this.to = '';
this.amount = 0;
this.memo = '';
}
}
export class SendPageState {
fromaddr: string;
toaddrs: ToAddr[];
constructor() {
this.fromaddr = '';
this.toaddrs = [];
}
}
export class ReceivePageState {
// A newly created address to show by default
newAddress: string;
// The key used for the receive page component.
// Increment to force re-render
rerenderKey: number;
constructor() {
this.newAddress = '';
this.rerenderKey = 0;
}
}
export class RPCConfig {
url: string;
constructor() {
this.url = '';
}
}
export class Info {
testnet: boolean;
latestBlock: number;
connections: number;
version: number;
verificationProgress: number;
currencyName: string;
solps: number;
zecPrice: number;
encrypted: boolean;
locked: boolean;
}
export class PasswordState {
showPassword: boolean;
confirmNeeded: boolean;
passwordCallback: (password: string) => void;
closeCallback: () => void;
helpText: string | null;
constructor() {
this.showPassword = false;
this.confirmNeeded = false;
this.passwordCallback = null;
this.closeCallback = null;
this.helpText = null;
}
}
export class ConnectedCompanionApp {
name: string;
lastSeen: number;
}
// eslint-disable-next-line max-classes-per-file
export default class AppState {
// The total confirmed and unconfirmed balance in this wallet
totalBalance: TotalBalance;
// The list of all t and z addresses that have a current balance. That is, the list of
// addresses that have a (confirmed or unconfirmed) UTXO or note pending.
addressesWithBalance: AddressBalance[];
// A map type that contains address -> privatekey mapping, for display on the receive page
// This mapping is ephemeral, and will disappear when the user navigates away.
addressPrivateKeys;
// List of all addresses in the wallet, including change addresses and addresses
// that don't have any balance or are unused
addresses: string[];
// List of Address / Label pairs
addressBook: AddressBookEntry[];
// List of all T and Z transactions
transactions: Transaction[];
// The state of the send page, as the user constructs a transaction
sendPageState: SendPageState;
// Any state for the receive page
receivePageState: ReceivePageState;
// The Current configuration of the RPC params
rpcConfig: RPCConfig;
// getinfo and getblockchaininfo result
info: Info;
// Is the app rescanning?
rescanning: boolean;
// Callbacks for the password dialog box
passwordState: PasswordState;
// The last seen connected companion app
connectedCompanionApp: ConnectedCompanionApp;
}

View File

@ -0,0 +1,39 @@
import React from 'react';
import cstyles from './Common.css';
import Utils from '../utils/utils';
// eslint-disable-next-line react/prop-types
export const BalanceBlockHighlight = ({ zecValue, usdValue, topLabel, currencyName }) => {
const { bigPart, smallPart } = Utils.splitZecAmountIntoBigSmall(zecValue);
return (
<div style={{ padding: '1em' }}>
{topLabel && <div className={[cstyles.small].join(' ')}>{topLabel}</div>}
<div className={[cstyles.highlight, cstyles.xlarge].join(' ')}>
<span>
{currencyName} {bigPart}
</span>
<span className={[cstyles.small, cstyles.zecsmallpart].join(' ')}>{smallPart}</span>
</div>
<div className={[cstyles.sublight, cstyles.small].join(' ')}>{usdValue}</div>
</div>
);
};
// eslint-disable-next-line react/prop-types
export const BalanceBlock = ({ zecValue, usdValue, topLabel, currencyName }) => {
const { bigPart, smallPart } = Utils.splitZecAmountIntoBigSmall(zecValue);
return (
<div className={cstyles.padall}>
<div className={[cstyles.small].join(' ')}>{topLabel}</div>
<div className={[cstyles.highlight, cstyles.large].join(' ')}>
<span>
{currencyName} {bigPart}
</span>
<span className={[cstyles.small, cstyles.zecsmallpart].join(' ')}>{smallPart}</span>
</div>
<div className={[cstyles.sublight, cstyles.small].join(' ')}>{usdValue}</div>
</div>
);
};

216
app/components/Common.css Normal file
View File

@ -0,0 +1,216 @@
.sidebarcontainer {
width: 220px;
float: left;
}
.contentcontainer {
width: calc(100% - 220px);
float: right;
}
.well {
background-color: #000;
padding: 16px;
}
.verticalflex {
flex-direction: column;
}
.flex {
display: flex;
}
.flexspacebetween {
display: flex;
justify-content: space-between;
}
.highlight {
color: #c3921f;
}
.xlarge {
font-size: 32px;
}
.large {
font-size: 18px;
}
.normal {
font-size: 14px;
}
.small {
font-size: 12px;
}
.fixedfont {
font-family: monospace;
}
.sublight {
color: #6a6a6a;
}
.center {
text-align: center;
}
.right {
text-align: right;
}
.red {
color: red;
}
.green {
color: green;
}
.yellow {
color: yellow;
}
.breakword {
word-break: break-all;
}
.primarybutton {
background-color: #c3921f;
color: #000;
font-weight: bold;
font-size: 14px;
padding-top: 8px;
padding-bottom: 8px;
padding-left: 16px;
padding-right: 16px;
border-radius: 4px;
border-color: #c3921f;
margin-left: 8px;
margin-right: 8px;
}
.primarybutton:disabled,
.primarybutton[disabled] {
border: 1px solid #999;
background-color: #ccc;
color: #666;
}
.primarybutton:hover {
cursor: pointer;
}
.padtopsmall {
padding-top: 4px;
}
.padbottomsmall {
padding-bottom: 4px;
}
.marginbottomsmall {
margin-bottom: 4px;
}
.margintopsmall {
margin-top: 4px;
}
.margintoplarge {
margin-top: 24px;
}
.marginleft {
margin-left: 16px;
}
.marginbottomlarge {
margin-bottom: 24px;
}
.padall {
padding: 16px;
}
.padsmallall {
padding: 4px;
}
.zecsmallpart {
padding-left: 2px;
}
.blackbg {
background-color: #000;
}
.maxwidth {
width: 100%;
}
.inputbox {
width: calc(100% - 16px);
font-family: Roboto, Arial, Helvetica, Helvetica Neue, serif;
min-height: 38px;
max-height: 300px;
padding-left: 8px;
padding-right: 8px;
font-size: 16px;
border: none;
border-bottom: grey 1px solid;
background-color: #000;
color: white;
}
.buttoncontainer {
text-align: center;
padding-top: 24px;
}
.modal {
top: 25%;
left: 12.5%;
right: 12.5%;
bottom: auto;
opacity: 1;
transform: translate(-0%, -12.5%);
background: #212124;
position: absolute;
padding: 16px;
min-width: 700px;
}
.modalOverlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(100, 100, 100, 0.5);
}
.accordionHeader:hover {
cursor: pointer;
}
.accordionHeader:focus {
outline: none;
}
.tableheader {
margin: 8px;
border-bottom: solid 1px grey;
}
.balancebox {
display: flex;
justify-content: space-between;
}
.containermargin {
margin: 16px;
}

View File

@ -0,0 +1,6 @@
.addressbalancecontainer {
display: flex;
flex-direction: column;
margin-left: 16px;
margin-right: 16px;
}

129
app/components/Dashboard.js Normal file
View File

@ -0,0 +1,129 @@
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable jsx-a11y/click-events-have-key-events */
/* eslint-disable no-plusplus */
/* eslint-disable react/prop-types */
// @flow
import React, { Component } from 'react';
import {
AccordionItemButton,
AccordionItem,
AccordionItemHeading,
AccordionItemPanel,
Accordion
} from 'react-accessible-accordion';
import styles from './Dashboard.css';
import cstyles from './Common.css';
import { TotalBalance, Info, AddressBalance } from './AppState';
import Utils from '../utils/utils';
import ScrollPane from './ScrollPane';
import { BalanceBlockHighlight, BalanceBlock } from './BalanceBlocks';
const AddressBalanceItem = ({ currencyName, zecPrice, item }) => {
const { bigPart, smallPart } = Utils.splitZecAmountIntoBigSmall(Math.abs(item.balance));
return (
<AccordionItem key={item.label} className={[cstyles.well, cstyles.margintopsmall].join(' ')} uuid={item.address}>
<AccordionItemHeading>
<AccordionItemButton className={cstyles.accordionHeader}>
<div className={[cstyles.flexspacebetween].join(' ')}>
<div>
<div>{Utils.splitStringIntoChunks(item.address, 6).join(' ')}</div>
{item.containsPending && (
<div className={[cstyles.red, cstyles.small, cstyles.padtopsmall].join(' ')}>
Some transactions are pending. Balances may change.
</div>
)}
</div>
<div className={[styles.txamount, cstyles.right].join(' ')}>
<div>
<span>
{currencyName} {bigPart}
</span>
<span className={[cstyles.small, cstyles.zecsmallpart].join(' ')}>{smallPart}</span>
</div>
<div className={[cstyles.sublight, cstyles.small, cstyles.padtopsmall].join(' ')}>
{Utils.getZecToUsdString(zecPrice, Math.abs(item.balance))}
</div>
</div>
</div>
</AccordionItemButton>
</AccordionItemHeading>
<AccordionItemPanel />
</AccordionItem>
);
};
type Props = {
totalBalance: TotalBalance,
info: Info,
addressesWithBalance: AddressBalance[]
};
export default class Home extends Component<Props> {
render() {
const { totalBalance, info, addressesWithBalance } = this.props;
const anyPending = addressesWithBalance && addressesWithBalance.find(i => i.containsPending);
return (
<div>
<div className={[cstyles.well, cstyles.containermargin].join(' ')}>
<div className={cstyles.balancebox}>
<BalanceBlockHighlight
zecValue={totalBalance.total}
usdValue={Utils.getZecToUsdString(info.zecPrice, totalBalance.total)}
currencyName={info.currencyName}
/>
<BalanceBlock
topLabel="Shielded"
zecValue={totalBalance.private}
usdValue={Utils.getZecToUsdString(info.zecPrice, totalBalance.private)}
currencyName={info.currencyName}
/>
<BalanceBlock
topLabel="Transparent"
zecValue={totalBalance.transparent}
usdValue={Utils.getZecToUsdString(info.zecPrice, totalBalance.transparent)}
currencyName={info.currencyName}
/>
</div>
<div>
{anyPending && (
<div className={[cstyles.red, cstyles.small, cstyles.padtopsmall].join(' ')}>
Some transactions are pending. Balances may change.
</div>
)}
</div>
</div>
<div className={styles.addressbalancecontainer}>
<ScrollPane offsetHeight={200}>
<div className={styles.addressbooklist}>
<div className={[cstyles.flexspacebetween, cstyles.tableheader, cstyles.sublight].join(' ')}>
<div>Address</div>
<div>Balance</div>
</div>
{addressesWithBalance &&
(addressesWithBalance.length === 0 ? (
<div className={[cstyles.center, cstyles.sublight].join(' ')}>No Addresses with a balance</div>
) : (
<Accordion>
{addressesWithBalance
.filter(ab => ab.balance > 0)
.map(ab => (
<AddressBalanceItem
key={ab.address}
item={ab}
currencyName={info.currencyName}
zecPrice={info.zecPrice}
/>
))}
</Accordion>
))}
</div>
</ScrollPane>
</div>
</div>
);
}
}

View File

@ -0,0 +1,46 @@
/* eslint-disable react/prop-types */
import Modal from 'react-modal';
import React from 'react';
import cstyles from './Common.css';
export class ErrorModalData {
title: string;
body: string;
modalIsOpen: boolean;
constructor() {
this.modalIsOpen = false;
}
}
export const ErrorModal = ({ title, body, modalIsOpen, closeModal }) => {
return (
<Modal
isOpen={modalIsOpen}
onRequestClose={closeModal}
className={cstyles.modal}
overlayClassName={cstyles.modalOverlay}
>
<div className={[cstyles.verticalflex].join(' ')}>
<div className={cstyles.marginbottomlarge} style={{ textAlign: 'center' }}>
{title}
</div>
<div
className={cstyles.well}
style={{ textAlign: 'center', wordBreak: 'break-all', maxHeight: '400px', overflowY: 'auto' }}
>
{body}
</div>
</div>
<div className={cstyles.buttoncontainer}>
<button type="button" className={cstyles.primarybutton} onClick={closeModal}>
Close
</button>
</div>
</Modal>
);
};

View File

@ -0,0 +1,10 @@
.loadingcontainer {
margin-left: -220px;
}
.newwalletcontainer {
flex-direction: column;
margin-left: 10%;
margin-right: 10%;
text-align: left;
}

View File

@ -0,0 +1,368 @@
/* eslint-disable radix */
/* eslint-disable max-classes-per-file */
import React, { Component } from 'react';
import { Redirect, withRouter } from 'react-router';
import { ipcRenderer } from 'electron';
import TextareaAutosize from 'react-textarea-autosize';
import native from '../../native/index.node';
import routes from '../constants/routes.json';
import { RPCConfig, Info } from './AppState';
import RPC from '../rpc';
import cstyles from './Common.css';
import styles from './LoadingScreen.css';
import Logo from '../assets/img/logobig.png';
type Props = {
setRPCConfig: (rpcConfig: RPCConfig) => void,
rescanning: boolean,
setRescanning: boolean => void,
setInfo: (info: Info) => void
};
class LoadingScreenState {
currentStatus: string;
loadingDone: boolean;
rpcConfig: RPCConfig | null;
url: string;
walletScreen: number; // 0 -> no wallet, load existing wallet 1 -> show option 2-> create new 3 -> restore existing
newWalletError: null | string; // Any errors when creating/restoring wallet
seed: string; // The new seed phrase for a newly created wallet or the seed phrase to restore from
birthday: number; // Wallet birthday if we're restoring
getinfoRetryCount: number;
constructor() {
this.currentStatus = 'Loading...';
this.loadingDone = false;
this.rpcConfig = null;
this.url = '';
this.getinfoRetryCount = 0;
this.walletScreen = 0;
this.newWalletError = null;
this.seed = '';
this.birthday = 0;
}
}
class LoadingScreen extends Component<Props, LoadingScreenState> {
constructor(props: Props) {
super(props);
const state = new LoadingScreenState();
state.url = 'https://lightwalletd.zecwallet.co:1443';
this.state = state;
}
componentDidMount() {
const { rescanning } = this.props;
if (rescanning) {
this.runSyncStatusPoller();
} else {
this.doFirstTimeSetup();
}
}
doFirstTimeSetup = async () => {
// Try to load the light client
const { url } = this.state;
// First, set up the exit handler
this.setupExitHandler();
// Test to see if the wallet exists
if (!native.litelib_wallet_exists('main')) {
// Show the wallet creation screen
this.setState({ walletScreen: 1 });
} else {
const result = native.litelib_initialize_existing(true, url);
console.log(`Intialization: ${result}`);
if (result !== 'OK') {
this.setState({
currentStatus: (
<span>
Error Initializing Lightclient
<br />
{result}
</span>
)
});
return;
}
this.getInfo();
}
};
setupExitHandler = () => {
// App is quitting, make sure to save the wallet properly.
ipcRenderer.on('appquitting', () => {
RPC.doSave();
// And reply that we're all done.
ipcRenderer.send('appquitdone');
});
};
getInfo() {
// Try getting the info.
try {
// Do a sync at start
this.setState({ currentStatus: 'Syncing...' });
// This will do the sync in another thread, so we have to check for sync status
RPC.doSync();
this.runSyncStatusPoller();
} catch (err) {
// Not yet finished loading. So update the state, and setup the next refresh
this.setState({ currentStatus: err });
}
}
runSyncStatusPoller = () => {
const me = this;
const { setRPCConfig, setInfo, setRescanning } = this.props;
const { url } = this.state;
const info = RPC.getInfoObject();
// And after a while, check the sync status.
const poller = setInterval(() => {
const syncstatus = RPC.doSyncStatus();
const ss = JSON.parse(syncstatus);
if (ss.syncing === 'false') {
// First, save the wallet so we don't lose the just-synced data
RPC.doSave();
// Set the info object, so the sidebar will show
console.log(info);
setInfo(info);
// This will cause a redirect to the dashboard
me.setState({ loadingDone: true });
setRescanning(false);
// Configure the RPC, which will setup the refresh
const rpcConfig = new RPCConfig();
rpcConfig.url = url;
setRPCConfig(rpcConfig);
// And cancel the updater
clearInterval(poller);
} else {
// Still syncing, grab the status and update the status
const p = ss.synced_blocks;
const t = ss.total_blocks;
const currentStatus = `Syncing ${p} / ${t}`;
me.setState({ currentStatus });
}
}, 1000);
};
createNewWallet = () => {
const { url } = this.state;
const result = native.litelib_initialize_new(true, url);
if (result.startsWith('Error')) {
this.setState({ newWalletError: result });
} else {
const r = JSON.parse(result);
this.setState({ walletScreen: 2, seed: r.seed });
}
};
startNewWallet = () => {
// Start using the new wallet
this.setState({ walletScreen: 0 });
this.getInfo();
};
restoreExistingWallet = () => {
this.setState({ walletScreen: 3 });
};
updateSeed = e => {
this.setState({ seed: e.target.value });
};
updateBirthday = e => {
this.setState({ birthday: e.target.value });
};
restoreWalletBack = () => {
// Reset the seed and birthday and try again
this.setState({ seed: '', birthday: 0, newWalletError: null, walletScreen: 3 });
};
doRestoreWallet = () => {
const { seed, birthday, url } = this.state;
console.log(`Restoring ${seed} with ${birthday}`);
const result = native.litelib_initialize_new_from_phrase(false, url, seed, parseInt(birthday));
if (result.startsWith('Error')) {
this.setState({ newWalletError: result });
} else {
this.setState({ walletScreen: 0 });
this.getInfo();
}
};
render() {
const { loadingDone, currentStatus, walletScreen, newWalletError, seed, birthday } = this.state;
// If still loading, show the status
if (!loadingDone) {
return (
<div className={[cstyles.verticalflex, cstyles.center, styles.loadingcontainer].join(' ')}>
{walletScreen === 0 && (
<div>
<div style={{ marginTop: '100px' }}>
<img src={Logo} width="200px;" alt="Logo" />
</div>
<div>{currentStatus}</div>
</div>
)}
{walletScreen === 1 && (
<div>
<div>
<img src={Logo} width="200px;" alt="Logo" />
</div>
<div className={[cstyles.well, styles.newwalletcontainer].join(' ')}>
<div className={cstyles.verticalflex}>
<div className={[cstyles.large, cstyles.highlight].join(' ')}>Create A New Wallet</div>
<div className={cstyles.padtopsmall}>
Creates a new wallet with a new randomly generated seed phrase. Please save the seed phrase
carefully, it&rsquo;s the only way to restore your wallet.
</div>
<div className={cstyles.margintoplarge}>
<button type="button" className={cstyles.primarybutton} onClick={this.createNewWallet}>
Create New
</button>
</div>
</div>
<div className={[cstyles.verticalflex, cstyles.margintoplarge].join(' ')}>
<div className={[cstyles.large, cstyles.highlight].join(' ')}>Restore Wallet From Seed</div>
<div className={cstyles.padtopsmall}>
If you already have a seed phrase, you can restore it to this wallet. This will rescan the
blockchain for all transactions from the seed phrase.
</div>
<div className={cstyles.margintoplarge}>
<button type="button" className={cstyles.primarybutton} onClick={this.restoreExistingWallet}>
Restore Existing
</button>
</div>
</div>
</div>
</div>
)}
{walletScreen === 2 && (
<div>
<div>
<img src={Logo} width="200px;" alt="Logo" />
</div>
<div className={[cstyles.well, styles.newwalletcontainer].join(' ')}>
<div className={cstyles.verticalflex}>
{newWalletError && (
<div>
<div className={[cstyles.large, cstyles.highlight].join(' ')}>Error Creating New Wallet</div>
<div className={cstyles.padtopsmall}>There was an error creating a new wallet</div>
<hr />
<div className={cstyles.padtopsmall}>{newWalletError}</div>
<hr />
</div>
)}
{!newWalletError && (
<div>
<div className={[cstyles.large, cstyles.highlight].join(' ')}>Your New Wallet</div>
<div className={cstyles.padtopsmall}>
This is your new wallet. Below is your seed phrase. PLEASE STORE IT CAREFULLY! The seed phrase
is the only way to recover your funds and transactions.
</div>
<hr />
<div className={cstyles.padtopsmall}>{seed}</div>
<hr />
<div className={cstyles.margintoplarge}>
<button type="button" className={cstyles.primarybutton} onClick={this.startNewWallet}>
Start Wallet
</button>
</div>
</div>
)}
</div>
</div>
</div>
)}
{walletScreen === 3 && (
<div>
<div>
<img src={Logo} width="200px;" alt="Logo" />
</div>
<div className={[cstyles.well, styles.newwalletcontainer].join(' ')}>
<div className={cstyles.verticalflex}>
{newWalletError && (
<div>
<div className={[cstyles.large, cstyles.highlight].join(' ')}>Error Restoring Wallet</div>
<div className={cstyles.padtopsmall}>There was an error restoring your seed phrase</div>
<hr />
<div className={cstyles.padtopsmall}>{newWalletError}</div>
<hr />
<div className={cstyles.margintoplarge}>
<button type="button" className={cstyles.primarybutton} onClick={this.restoreWalletBack}>
Back
</button>
</div>
</div>
)}
{!newWalletError && (
<div>
<div className={[cstyles.large].join(' ')}>Please enter your seed phrase</div>
<TextareaAutosize className={cstyles.inputbox} value={seed} onChange={e => this.updateSeed(e)} />
<div className={[cstyles.large, cstyles.margintoplarge].join(' ')}>
Wallet Birthday. If you don&rsquo;t know this, it is OK to enter &lsquo;0&rsquo;
</div>
<input
type="number"
className={cstyles.inputbox}
value={birthday}
onChange={e => this.updateBirthday(e)}
/>
<div className={cstyles.margintoplarge}>
<button type="button" className={cstyles.primarybutton} onClick={this.doRestoreWallet}>
Restore Wallet
</button>
</div>
</div>
)}
</div>
</div>
</div>
)}
</div>
);
}
return <Redirect to={routes.DASHBOARD} />;
}
}
export default withRouter(LoadingScreen);

View File

@ -0,0 +1,105 @@
// @flow
import React, { PureComponent } from 'react';
import Modal from 'react-modal';
import cstyles from './Common.css';
type Props = {
modalIsOpen: boolean,
confirmNeeded: boolean,
passwordCallback: (password: string) => void,
closeCallback: () => void,
helpText: string | null
};
type State = {
password: string,
confirmPassword: string
};
export default class PasswordModal extends PureComponent<Props, State> {
constructor(props: Props) {
super(props);
this.state = { password: '', confirmPassword: '' };
}
enterButton = () => {
const { passwordCallback } = this.props;
const { password } = this.state;
passwordCallback(password);
// Clear the passwords
this.setState({ password: '', confirmPassword: '' });
};
closeButton = () => {
const { closeCallback } = this.props;
closeCallback();
// Clear the passwords
this.setState({ password: '', confirmPassword: '' });
};
render() {
const { modalIsOpen, confirmNeeded, helpText } = this.props;
const { password, confirmPassword } = this.state;
const enabled = !confirmNeeded || password === confirmPassword;
return (
<Modal
isOpen={modalIsOpen}
onRequestClose={this.closeButton}
className={cstyles.modal}
overlayClassName={cstyles.modalOverlay}
>
<div className={[cstyles.verticalflex].join(' ')}>
<div className={cstyles.marginbottomlarge} style={{ textAlign: 'left' }}>
{helpText && <span>{helpText}</span>}
{!helpText && <span>Enter Wallet Password</span>}
</div>
<div className={cstyles.well} style={{ textAlign: 'left' }}>
<div className={cstyles.sublight}>Password</div>
<input
type="password"
className={[cstyles.inputbox, cstyles.marginbottomlarge].join(' ')}
value={password}
onChange={e => this.setState({ password: e.target.value })}
/>
{confirmNeeded && (
<div>
<div className={cstyles.sublight}>Confirm Password</div>
<input
type="password"
className={[cstyles.inputbox, cstyles.marginbottomlarge].join(' ')}
value={confirmPassword}
onChange={e => this.setState({ confirmPassword: e.target.value })}
/>
</div>
)}
</div>
<div className={cstyles.buttoncontainer}>
{!enabled && <div className={[cstyles.red].join(' ')}>Passwords do not match</div>}
<button
type="button"
className={[cstyles.primarybutton, cstyles.margintoplarge].join(' ')}
onClick={this.enterButton}
disabled={!enabled}
>
Enter
</button>
<button type="button" className={cstyles.primarybutton} onClick={this.closeButton}>
Cancel
</button>
</div>
</div>
</Modal>
);
}
}

View File

@ -0,0 +1,18 @@
.receivecontainer {
margin: 16px;
}
.receiveblock {
margin-bottom: 16px;
display: flex;
flex-direction: column;
}
.receiveDetail {
margin-top: 16px;
}
.receiveQrcode {
padding: 8px;
background-color: white;
}

243
app/components/Receive.js Normal file
View File

@ -0,0 +1,243 @@
/* eslint-disable react/prop-types */
import React, { Component, useState } from 'react';
import { Tab, Tabs, TabList, TabPanel } from 'react-tabs';
import {
Accordion,
AccordionItem,
AccordionItemHeading,
AccordionItemButton,
AccordionItemPanel
} from 'react-accessible-accordion';
import QRCode from 'qrcode.react';
import { shell, clipboard } from 'electron';
import styles from './Receive.css';
import cstyles from './Common.css';
import Utils from '../utils/utils';
import { AddressBalance, Info, ReceivePageState, AddressBookEntry } from './AppState';
import ScrollPane from './ScrollPane';
const AddressBlock = ({ addressBalance, label, currencyName, zecPrice, privateKey, fetchAndSetSinglePrivKey }) => {
const { address } = addressBalance;
const [copied, setCopied] = useState(false);
const balance = addressBalance.balance || 0;
const openAddress = () => {
if (currencyName === 'TAZ') {
shell.openExternal(`https://chain.so/address/ZECTEST/${address}`);
} else {
shell.openExternal(`https://zcha.in/accounts/${address}`);
}
};
return (
<AccordionItem key={copied} className={[cstyles.well, styles.receiveblock].join(' ')} uuid={address}>
<AccordionItemHeading>
<AccordionItemButton className={cstyles.accordionHeader}>{address}</AccordionItemButton>
</AccordionItemHeading>
<AccordionItemPanel className={[styles.receiveDetail].join(' ')}>
<div className={[cstyles.flexspacebetween].join(' ')}>
<div className={[cstyles.verticalflex, cstyles.marginleft].join(' ')}>
{label && (
<div className={cstyles.margintoplarge}>
<div className={[cstyles.sublight].join(' ')}>Label</div>
<div className={[cstyles.padtopsmall, cstyles.fixedfont].join(' ')}>{label}</div>
</div>
)}
<div className={[cstyles.sublight, cstyles.margintoplarge].join(' ')}>Funds</div>
<div className={[cstyles.padtopsmall].join(' ')}>
{currencyName} {balance}
</div>
<div className={[cstyles.padtopsmall].join(' ')}>{Utils.getZecToUsdString(zecPrice, balance)}</div>
<div className={[cstyles.margintoplarge, cstyles.breakword].join(' ')}>
{privateKey && (
<div>
<div className={[cstyles.sublight].join(' ')}>Private Key</div>
<div
className={[cstyles.breakword, cstyles.padtopsmall, cstyles.fixedfont].join(' ')}
style={{ maxWidth: '600px' }}
>
{privateKey}
</div>
</div>
)}
</div>
<div>
<button
className={[cstyles.primarybutton, cstyles.margintoplarge].join(' ')}
type="button"
onClick={() => {
clipboard.writeText(address);
setCopied(true);
setTimeout(() => setCopied(false), 5000);
}}
>
{copied ? <span>Copied!</span> : <span>Copy Address</span>}
</button>
{!privateKey && (
<button
className={[cstyles.primarybutton].join(' ')}
type="button"
onClick={() => fetchAndSetSinglePrivKey(address)}
>
Export Private Key
</button>
)}
{Utils.isTransparent(address) && (
<button className={[cstyles.primarybutton].join(' ')} type="button" onClick={() => openAddress()}>
View on explorer <i className={['fas', 'fa-external-link-square-alt'].join(' ')} />
</button>
)}
</div>
</div>
<div>
<QRCode value={address} className={[styles.receiveQrcode].join(' ')} />
</div>
</div>
</AccordionItemPanel>
</AccordionItem>
);
};
type Props = {
addresses: string[],
addressesWithBalance: AddressBalance[],
addressBook: AddressBookEntry[],
info: Info,
receivePageState: ReceivePageState,
fetchAndSetSinglePrivKey: string => void,
createNewAddress: boolean => void,
rerenderKey: number
};
export default class Receive extends Component<Props> {
render() {
const {
addresses,
addressesWithBalance,
addressPrivateKeys,
addressBook,
info,
receivePageState,
fetchAndSetSinglePrivKey,
createNewAddress,
rerenderKey
} = this.props;
// Convert the addressBalances into a map.
const addressMap = addressesWithBalance.reduce((map, a) => {
// eslint-disable-next-line no-param-reassign
map[a.address] = a.balance;
return map;
}, {});
const zaddrs = addresses
.filter(a => Utils.isSapling(a))
.slice(0, 100)
.map(a => new AddressBalance(a, addressMap[a]));
let defaultZaddr = zaddrs.length ? zaddrs[0].address : '';
if (receivePageState && Utils.isSapling(receivePageState.newAddress)) {
defaultZaddr = receivePageState.newAddress;
// move this address to the front, since the scrollbar will reset when we re-render
zaddrs.sort((x, y) => {
// eslint-disable-next-line prettier/prettier, no-nested-ternary
return x.address === defaultZaddr ? -1 : y.address === defaultZaddr ? 1 : 0
});
}
const taddrs = addresses
.filter(a => Utils.isTransparent(a))
.slice(0, 100)
.map(a => new AddressBalance(a, addressMap[a]));
let defaultTaddr = taddrs.length ? taddrs[0].address : '';
if (receivePageState && Utils.isTransparent(receivePageState.newAddress)) {
defaultTaddr = receivePageState.newAddress;
// move this address to the front, since the scrollbar will reset when we re-render
taddrs.sort((x, y) => {
// eslint-disable-next-line prettier/prettier, no-nested-ternary
return x.address === defaultTaddr ? -1 : y.address === defaultTaddr ? 1 : 0
});
}
const addressBookMap = addressBook.reduce((map, obj) => {
// eslint-disable-next-line no-param-reassign
map[obj.address] = obj.label;
return map;
}, {});
return (
<div>
<div className={styles.receivecontainer}>
<Tabs>
<TabList>
<Tab>Shielded</Tab>
<Tab>Transparent</Tab>
</TabList>
<TabPanel key={`z${rerenderKey}`}>
{/* Change the hardcoded height */}
<ScrollPane offsetHeight={100}>
<Accordion preExpanded={[defaultZaddr]}>
{zaddrs.map(a => (
<AddressBlock
key={a.address}
addressBalance={a}
currencyName={info.currencyName}
label={addressBookMap[a.address]}
zecPrice={info.zecPrice}
privateKey={addressPrivateKeys[a.address]}
fetchAndSetSinglePrivKey={fetchAndSetSinglePrivKey}
rerender={this.rerender}
/>
))}
</Accordion>
<button
className={[cstyles.primarybutton, cstyles.margintoplarge, cstyles.marginbottomlarge].join(' ')}
onClick={() => createNewAddress(true)}
type="button"
>
New Shielded Address
</button>
</ScrollPane>
</TabPanel>
<TabPanel key={`t${rerenderKey}`}>
{/* Change the hardcoded height */}
<ScrollPane offsetHeight={100}>
<Accordion preExpanded={[defaultTaddr]}>
{taddrs.map(a => (
<AddressBlock
key={a.address}
addressBalance={a}
currencyName={info.currencyName}
zecPrice={info.zecPrice}
privateKey={addressPrivateKeys[a.address]}
fetchAndSetSinglePrivKey={fetchAndSetSinglePrivKey}
rerender={this.rerender}
/>
))}
</Accordion>
<button
className={[cstyles.primarybutton, cstyles.margintoplarge, cstyles.marginbottomlarge].join(' ')}
type="button"
onClick={() => createNewAddress(false)}
>
New Transparent Address
</button>
</ScrollPane>
</TabPanel>
</Tabs>
</div>
</div>
);
}
}

View File

@ -0,0 +1,49 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
type PaneState = {
height: number
};
type Props = {
children: PropTypes.node.isRequired,
className: PropTypes.node.isRequired,
offsetHeight: number
};
export default class ScrollPane extends Component<Props, PaneState> {
constructor(props: Props) {
super(props);
this.state = { height: 0 };
}
componentDidMount() {
this.updateDimensions();
window.addEventListener('resize', this.updateDimensions.bind(this));
}
componentWillUnmount() {
window.removeEventListener('resize', this.updateDimensions.bind(this));
}
/**
* Calculate & Update state of height, needed for the scrolling
*/
updateDimensions() {
// eslint-disable-next-line react/destructuring-assignment
const updateHeight = window.innerHeight - this.props.offsetHeight;
this.setState({ height: updateHeight });
}
render() {
const { children, className } = this.props;
const { height } = this.state;
return (
<div className={className} style={{ overflowY: 'auto', overflowX: 'hidden', height }}>
{children}
</div>
);
}
}

39
app/components/Send.css Normal file
View File

@ -0,0 +1,39 @@
.toaddrcontainer {
overflow-y: auto;
}
.toaddrbutton {
height: 24px;
margin-top: 13px;
}
.toaddrbutton:hover {
cursor: pointer;
}
.confirmModal {
top: 25%;
left: 25%;
right: auto;
bottom: auto;
opacity: 1;
transform: translate(-12.5%, -12.5%);
background: #212124;
position: absolute;
padding: 16px;
min-width: 700px;
}
.confirmModalAddress {
width: 60%;
padding-bottom: 8px;
}
.confirmOverlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(100, 100, 100, 0.5);
}

585
app/components/Send.js Normal file
View File

@ -0,0 +1,585 @@
/* eslint-disable no-restricted-globals */
/* eslint-disable no-else-return */
/* eslint-disable radix */
/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */
/* eslint-disable jsx-a11y/click-events-have-key-events */
/* eslint-disable react/prop-types */
/* eslint-disable max-classes-per-file */
// @flow
import React, { PureComponent } from 'react';
import Modal from 'react-modal';
import TextareaAutosize from 'react-textarea-autosize';
import { withRouter } from 'react-router-dom';
import styles from './Send.css';
import cstyles from './Common.css';
import { ToAddr, AddressBalance, SendPageState, Info, AddressBookEntry, TotalBalance } from './AppState';
import Utils from '../utils/utils';
import ScrollPane from './ScrollPane';
import ArrowUpLight from '../assets/img/arrow_up_dark.png';
import { ErrorModal } from './ErrorModal';
import { BalanceBlockHighlight } from './BalanceBlocks';
import routes from '../constants/routes.json';
type OptionType = {
value: string,
label: string
};
const Spacer = () => {
return <div style={{ marginTop: '24px' }} />;
};
// $FlowFixMe
const ToAddrBox = ({
toaddr,
zecPrice,
updateToField,
fromAddress,
fromAmount,
setMaxAmount,
setSendButtonEnable,
totalAmountAvailable
}) => {
const isMemoDisabled = !Utils.isZaddr(toaddr.to);
const addressIsValid = toaddr.to === '' || Utils.isZaddr(toaddr.to) || Utils.isTransparent(toaddr.to);
const memoIsValid = toaddr.memo.length <= 512;
let amountError = null;
if (toaddr.amount) {
if (toaddr.amount < 0) {
amountError = 'Amount cannot be negative';
}
if (toaddr.amount > fromAmount) {
amountError = 'Amount Exceeds Balance';
}
const s = toaddr.amount.toString().split('.');
if (s && s.length > 1 && s[1].length > 8) {
amountError = 'Too Many Decimals';
}
}
if (isNaN(toaddr.amount)) {
// Amount is empty
amountError = 'Amount cannot be empty';
}
if (
!addressIsValid ||
amountError ||
!memoIsValid ||
toaddr.to === '' ||
parseFloat(toaddr.amount) === 0 ||
fromAmount === 0
) {
setSendButtonEnable(false);
} else {
setSendButtonEnable(true);
}
const usdValue = Utils.getZecToUsdString(zecPrice, toaddr.amount);
const addReplyTo = () => {
if (toaddr.memo.endsWith(fromAddress)) {
return;
}
if (fromAddress) {
updateToField(toaddr.id, null, null, `${toaddr.memo}\nReply-To:\n${fromAddress}`);
}
};
return (
<div>
<div className={[cstyles.well, cstyles.verticalflex].join(' ')}>
<div className={[cstyles.flexspacebetween].join(' ')}>
<div className={cstyles.sublight}>To</div>
<div className={cstyles.validationerror}>
{addressIsValid ? (
<i className={[cstyles.green, 'fas', 'fa-check'].join(' ')} />
) : (
<span className={cstyles.red}>Invalid Address</span>
)}
</div>
</div>
<input
type="text"
placeholder="Z or T address"
className={cstyles.inputbox}
value={toaddr.to}
onChange={e => updateToField(toaddr.id, e, null, null)}
/>
<Spacer />
<div className={[cstyles.flexspacebetween].join(' ')}>
<div className={cstyles.sublight}>Amount</div>
<div className={cstyles.validationerror}>
{amountError ? <span className={cstyles.red}>{amountError}</span> : <span>{usdValue}</span>}
</div>
</div>
<div className={[cstyles.flexspacebetween].join(' ')}>
<input
type="number"
className={cstyles.inputbox}
value={toaddr.amount}
onChange={e => updateToField(toaddr.id, null, e, null)}
/>
<img
className={styles.toaddrbutton}
src={ArrowUpLight}
alt="Max"
onClick={() => setMaxAmount(toaddr.id, totalAmountAvailable)}
/>
</div>
<Spacer />
{isMemoDisabled && <div className={cstyles.sublight}>Memos only for z-addresses</div>}
{!isMemoDisabled && (
<div>
<div className={[cstyles.flexspacebetween].join(' ')}>
<div className={cstyles.sublight}>Memo</div>
<div className={cstyles.validationerror}>
{memoIsValid ? toaddr.memo.length : <span className={cstyles.red}>{toaddr.memo.length}</span>} / 512
</div>
</div>
<TextareaAutosize
className={cstyles.inputbox}
value={toaddr.memo}
disabled={isMemoDisabled}
onChange={e => updateToField(toaddr.id, null, null, e)}
/>
<input type="checkbox" onChange={e => e.target.checked && addReplyTo()} />
Include Reply-To address
</div>
)}
<Spacer />
</div>
<Spacer />
</div>
);
};
function getSendManyJSON(sendPageState: SendPageState): [] {
const json = sendPageState.toaddrs.map(to => {
const memo = to.memo || '';
const amount = parseInt((parseFloat(to.amount) * 10 ** 8).toFixed(0));
if (memo === '') {
return { address: to.to, amount };
} else {
return { address: to.to, amount, memo };
}
});
console.log('Sending:');
console.log(json);
return json;
}
const ConfirmModalToAddr = ({ toaddr, info }) => {
const { bigPart, smallPart } = Utils.splitZecAmountIntoBigSmall(toaddr.amount);
const memo: string = toaddr.memo ? toaddr.memo : '';
return (
<div className={cstyles.well}>
<div className={[cstyles.flexspacebetween, cstyles.margintoplarge].join(' ')}>
<div className={[styles.confirmModalAddress].join(' ')}>
{Utils.splitStringIntoChunks(toaddr.to, 6).join(' ')}
</div>
<div className={[cstyles.verticalflex, cstyles.right].join(' ')}>
<div className={cstyles.large}>
<div>
<span>
{info.currencyName} {bigPart}
</span>
<span className={[cstyles.small, styles.zecsmallpart].join(' ')}>{smallPart}</span>
</div>
</div>
<div>{Utils.getZecToUsdString(info.zecPrice, toaddr.amount)}</div>
</div>
</div>
<div className={[cstyles.sublight, cstyles.breakword].join(' ')}>{memo}</div>
</div>
);
};
// Internal because we're using withRouter just below
const ConfirmModalInternal = ({
sendPageState,
info,
sendTransaction,
clearToAddrs,
closeModal,
modalIsOpen,
openErrorModal,
openPasswordAndUnlockIfNeeded,
history
}) => {
const sendingTotal = sendPageState.toaddrs.reduce((s, t) => parseFloat(s) + parseFloat(t.amount), 0.0) + 0.0001;
const { bigPart, smallPart } = Utils.splitZecAmountIntoBigSmall(sendingTotal);
const sendButton = () => {
// First, close the confirm modal.
closeModal();
// This will be replaced by either a success TXID or error message that the user
// has to close manually.
openErrorModal('Computing Transaction', 'Please wait...This could take a while');
// Now, send the Tx in a timeout, so that the error modal above has a chance to display
setTimeout(() => {
openPasswordAndUnlockIfNeeded(() => {
// Then send the Tx async
(async () => {
const sendJson = getSendManyJSON(sendPageState);
let txid = '';
try {
txid = sendTransaction(sendJson);
openErrorModal(
'Successfully Broadcast Transaction',
`Transaction was successfully broadcast.\nTXID: ${txid}`
);
clearToAddrs();
// Redirect to dashboard after
history.push(routes.DASHBOARD);
} catch (err) {
// If there was an error, show the error modal
openErrorModal('Error Sending Transaction', err);
}
})();
});
}, 10);
};
return (
<Modal
isOpen={modalIsOpen}
onRequestClose={closeModal}
className={styles.confirmModal}
overlayClassName={styles.confirmOverlay}
>
<div className={[cstyles.verticalflex].join(' ')}>
<div className={[cstyles.marginbottomlarge, cstyles.center].join(' ')}>Confirm Transaction</div>
<div className={cstyles.flex}>
<div
className={[
cstyles.highlight,
cstyles.xlarge,
cstyles.flexspacebetween,
cstyles.well,
cstyles.maxwidth
].join(' ')}
>
<div>Total</div>
<div className={[cstyles.right, cstyles.verticalflex].join(' ')}>
<div>
<span>
{info.currencyName} {bigPart}
</span>
<span className={[cstyles.small, styles.zecsmallpart].join(' ')}>{smallPart}</span>
</div>
<div className={cstyles.normal}>{Utils.getZecToUsdString(info.zecPrice, sendingTotal)}</div>
</div>
</div>
</div>
<div className={[cstyles.verticalflex, cstyles.margintoplarge].join(' ')}>
{sendPageState.toaddrs.map(t => (
<ConfirmModalToAddr key={t.to} toaddr={t} info={info} />
))}
</div>
<ConfirmModalToAddr toaddr={{ to: 'Fee', amount: 0.0001, memo: null }} info={info} />
<div className={cstyles.buttoncontainer}>
<button type="button" className={cstyles.primarybutton} onClick={() => sendButton()}>
Send
</button>
<button type="button" className={cstyles.primarybutton} onClick={closeModal}>
Cancel
</button>
</div>
</div>
</Modal>
);
};
const ConfirmModal = withRouter(ConfirmModalInternal);
type Props = {
addresses: string[],
totalBalance: TotalBalance,
addressBook: AddressBookEntry[],
sendPageState: SendPageState,
sendTransaction: (sendJson: []) => string,
setSendPageState: (sendPageState: SendPageState) => void,
openErrorModal: (title: string, body: string) => void,
closeErrorModal: () => void,
info: Info,
openPasswordAndUnlockIfNeeded: (successCallback: () => void) => void
};
class SendState {
modalIsOpen: boolean;
errorModalIsOpen: boolean;
errorModalTitle: string;
errorModalBody: string;
sendButtonEnabled: boolean;
constructor() {
this.modalIsOpen = false;
this.errorModalIsOpen = false;
this.errorModalBody = '';
this.errorModalTitle = '';
this.sendButtonEnabled = false;
}
}
export default class Send extends PureComponent<Props, SendState> {
constructor(props: Props) {
super(props);
this.state = new SendState();
}
addToAddr = () => {
const { sendPageState, setSendPageState } = this.props;
const newToAddrs = sendPageState.toaddrs.concat(new ToAddr(Utils.getNextToAddrID()));
// Create the new state object
const newState = new SendPageState();
newState.fromaddr = sendPageState.fromaddr;
newState.toaddrs = newToAddrs;
setSendPageState(newState);
};
clearToAddrs = () => {
const { sendPageState, setSendPageState } = this.props;
const newToAddrs = [new ToAddr(Utils.getNextToAddrID())];
// Create the new state object
const newState = new SendPageState();
newState.fromaddr = sendPageState.fromaddr;
newState.toaddrs = newToAddrs;
setSendPageState(newState);
};
changeFrom = (selectedOption: OptionType) => {
const { sendPageState, setSendPageState } = this.props;
// Create the new state object
const newState = new SendPageState();
newState.fromaddr = selectedOption.value;
newState.toaddrs = sendPageState.toaddrs;
setSendPageState(newState);
};
updateToField = (id: number, address: Event | null, amount: Event | null, memo: Event | string | null) => {
const { sendPageState, setSendPageState } = this.props;
const newToAddrs = sendPageState.toaddrs.slice(0);
// Find the correct toAddr
const toAddr = newToAddrs.find(a => a.id === id);
if (address) {
// $FlowFixMe
toAddr.to = address.target.value.replace(/ /g, ''); // Remove spaces
}
if (amount) {
// Check to see the new amount if valid
// $FlowFixMe
const newAmount = parseFloat(amount.target.value);
if (newAmount < 0 || newAmount > 21 * 10 ** 6) {
return;
}
// $FlowFixMe
toAddr.amount = newAmount;
}
if (memo) {
if (typeof memo === 'string') {
toAddr.memo = memo;
} else {
// $FlowFixMe
toAddr.memo = memo.target.value;
}
}
// Create the new state object
const newState = new SendPageState();
newState.fromaddr = sendPageState.fromaddr;
newState.toaddrs = newToAddrs;
setSendPageState(newState);
};
setMaxAmount = (id: number, total: number) => {
const { sendPageState, setSendPageState } = this.props;
const newToAddrs = sendPageState.toaddrs.slice(0);
let totalOtherAmount: number = newToAddrs
.filter(a => a.id !== id)
.reduce((s, a) => parseFloat(s) + parseFloat(a.amount), 0);
// Add Fee
totalOtherAmount += 0.0001;
// Find the correct toAddr
const toAddr = newToAddrs.find(a => a.id === id);
toAddr.amount = total - totalOtherAmount;
if (toAddr.amount < 0) toAddr.amount = 0;
toAddr.amount = Utils.maxPrecision(toAddr.amount);
// Create the new state object
const newState = new SendPageState();
newState.fromaddr = sendPageState.fromaddr;
newState.toaddrs = newToAddrs;
setSendPageState(newState);
};
setSendButtonEnable = (sendButtonEnabled: boolean) => {
this.setState({ sendButtonEnabled });
};
openModal = () => {
this.setState({ modalIsOpen: true });
};
closeModal = () => {
this.setState({ modalIsOpen: false });
};
getBalanceForAddress = (addr: string, addressesWithBalance: AddressBalance[]): number => {
// Find the addr in addressesWithBalance
const addressBalance: AddressBalance = addressesWithBalance.find(ab => ab.address === addr);
if (!addressBalance) {
return 0;
}
return addressBalance.balance;
};
getLabelForFromAddress = (addr: string, addressesWithBalance: AddressBalance[], currencyName: string) => {
// Find the addr in addressesWithBalance
const { addressBook } = this.props;
const label = addressBook.find(ab => ab.address === addr);
const labelStr = label ? ` [ ${label.label} ]` : '';
const balance = this.getBalanceForAddress(addr, addressesWithBalance);
return `[ ${currencyName} ${balance.toString()} ]${labelStr} ${addr}`;
};
render() {
const { modalIsOpen, errorModalIsOpen, errorModalTitle, errorModalBody, sendButtonEnabled } = this.state;
const {
addresses,
sendTransaction,
sendPageState,
info,
totalBalance,
openErrorModal,
closeErrorModal,
openPasswordAndUnlockIfNeeded
} = this.props;
const totalAmountAvailable = totalBalance.transparent + totalBalance.verifiedPrivate;
const fromaddr = addresses.find(a => Utils.isSapling(a));
return (
<div>
<div className={[cstyles.xlarge, cstyles.padall, cstyles.center].join(' ')}>Send</div>
<div className={styles.sendcontainer}>
<div className={[cstyles.well, cstyles.balancebox, cstyles.containermargin].join(' ')}>
<BalanceBlockHighlight
topLabel="Confirmed Funds"
zecValue={totalAmountAvailable}
usdValue={Utils.getZecToUsdString(info.zecPrice, totalAmountAvailable)}
currencyName={info.currencyName}
/>
<BalanceBlockHighlight
topLabel="All Funds"
zecValue={totalBalance.total}
usdValue={Utils.getZecToUsdString(info.zecPrice, totalBalance.total)}
currencyName={info.currencyName}
/>
</div>
<ScrollPane className={cstyles.containermargin} offsetHeight={320}>
{sendPageState.toaddrs.map(toaddr => {
return (
<ToAddrBox
key={toaddr.id}
toaddr={toaddr}
zecPrice={info.zecPrice}
updateToField={this.updateToField}
fromAddress={fromaddr}
fromAmount={totalAmountAvailable}
setMaxAmount={this.setMaxAmount}
setSendButtonEnable={this.setSendButtonEnable}
totalAmountAvailable={totalAmountAvailable}
/>
);
})}
<div style={{ textAlign: 'right' }}>
<button type="button" onClick={this.addToAddr}>
<i className={['fas', 'fa-plus'].join(' ')} />
</button>
</div>
</ScrollPane>
<div className={cstyles.center}>
<button
type="button"
disabled={!sendButtonEnabled}
className={cstyles.primarybutton}
onClick={this.openModal}
>
Send
</button>
<button type="button" className={cstyles.primarybutton} onClick={this.clearToAddrs}>
Cancel
</button>
</div>
<ConfirmModal
sendPageState={sendPageState}
info={info}
sendTransaction={sendTransaction}
openErrorModal={openErrorModal}
closeModal={this.closeModal}
modalIsOpen={modalIsOpen}
clearToAddrs={this.clearToAddrs}
openPasswordAndUnlockIfNeeded={openPasswordAndUnlockIfNeeded}
/>
<ErrorModal
title={errorModalTitle}
body={errorModalBody}
modalIsOpen={errorModalIsOpen}
closeModal={closeErrorModal}
/>
</div>
</div>
);
}
}

View File

@ -0,0 +1,25 @@
.sidebar {
background-color: #000;
height: calc(100vh - 135px);
}
.sidebarmenuitem {
height: 40px;
padding: 10px;
margin-left: 16px;
font-size: 20px;
}
.sidebarmenuitemactive {
color: #c3921f;
}
.sidebarlogobg {
background: #ffb100;
margin-bottom: 8px;
}
.exportedPrivKeys {
width: 100%;
max-height: calc(100vh - 400px);
}

513
app/components/Sidebar.js Normal file
View File

@ -0,0 +1,513 @@
/* eslint-disable react/destructuring-assignment */
/* eslint-disable react/prop-types */
import React, { PureComponent } from 'react';
import type { Element } from 'react';
import url from 'url';
import querystring from 'querystring';
import Modal from 'react-modal';
import { withRouter } from 'react-router';
import { Link } from 'react-router-dom';
import { ipcRenderer } from 'electron';
import TextareaAutosize from 'react-textarea-autosize';
import PropTypes from 'prop-types';
import styles from './Sidebar.css';
import cstyles from './Common.css';
import routes from '../constants/routes.json';
import Logo from '../assets/img/logobig.png';
import { Info } from './AppState';
import Utils from '../utils/utils';
import RPC from '../rpc';
const ExportPrivKeyModal = ({ modalIsOpen, exportedPrivKeys, closeModal }) => {
return (
<Modal
isOpen={modalIsOpen}
onRequestClose={closeModal}
className={cstyles.modal}
overlayClassName={cstyles.modalOverlay}
>
<div className={[cstyles.verticalflex].join(' ')}>
<div className={cstyles.marginbottomlarge} style={{ textAlign: 'center' }}>
Your Wallet Private Keys
</div>
<div className={[cstyles.marginbottomlarge, cstyles.center].join(' ')}>
These are all the private keys in your wallet. Please store them carefully!
</div>
{exportedPrivKeys && (
<TextareaAutosize value={exportedPrivKeys.join('\n')} className={styles.exportedPrivKeys} disabled />
)}
</div>
<div className={cstyles.buttoncontainer}>
<button type="button" className={cstyles.primarybutton} onClick={closeModal}>
Close
</button>
</div>
</Modal>
);
};
const PayURIModal = ({
modalIsOpen,
modalInput,
setModalInput,
closeModal,
modalTitle,
actionButtonName,
actionCallback
}) => {
return (
<Modal
isOpen={modalIsOpen}
onRequestClose={closeModal}
className={cstyles.modal}
overlayClassName={cstyles.modalOverlay}
>
<div className={[cstyles.verticalflex].join(' ')}>
<div className={cstyles.marginbottomlarge} style={{ textAlign: 'center' }}>
{modalTitle}
</div>
<div className={cstyles.well} style={{ textAlign: 'center' }}>
<input
type="text"
className={cstyles.inputbox}
placeholder="URI"
value={modalInput}
onChange={e => setModalInput(e.target.value)}
/>
</div>
</div>
<div className={cstyles.buttoncontainer}>
{actionButtonName && (
<button
type="button"
className={cstyles.primarybutton}
onClick={() => {
if (modalInput) {
actionCallback(modalInput);
}
closeModal();
}}
>
{actionButtonName}
</button>
)}
<button type="button" className={cstyles.primarybutton} onClick={closeModal}>
Close
</button>
</div>
</Modal>
);
};
const SidebarMenuItem = ({ name, routeName, currentRoute, iconname }) => {
let isActive = false;
if ((currentRoute.endsWith('app.html') && routeName === routes.HOME) || currentRoute === routeName) {
isActive = true;
}
let activeColorClass = '';
if (isActive) {
activeColorClass = styles.sidebarmenuitemactive;
}
return (
<div className={[styles.sidebarmenuitem, activeColorClass].join(' ')}>
<Link to={routeName}>
<span className={activeColorClass}>
<i className={['fas', iconname].join(' ')} />
&nbsp; &nbsp;
{name}
</span>
</Link>
</div>
);
};
type Props = {
info: Info,
setRescanning: boolean => void,
addresses: string[],
setInfo: Info => void,
setSendTo: (address: string, amount: number | null, memo: string | null) => void,
getPrivKeyAsString: (address: string) => string,
history: PropTypes.object.isRequired,
openErrorModal: (title: string, body: string | Element<'div'>) => void,
openPassword: (boolean, (string) => void, () => void, string) => void,
openPasswordAndUnlockIfNeeded: (successCallback: () => void) => void,
lockWallet: () => void,
encryptWallet: string => void,
decryptWallet: string => void
};
type State = {
uriModalIsOpen: boolean,
uriModalInputValue: string | null,
exportPrivKeysModalIsOpen: boolean,
exportedPrivKeys: string[] | null
};
class Sidebar extends PureComponent<Props, State> {
constructor(props) {
super(props);
this.state = {
uriModalIsOpen: false,
uriModalInputValue: null,
exportPrivKeysModalIsOpen: false,
exportedPrivKeys: null
};
this.setupMenuHandlers();
}
// Handle menu items
setupMenuHandlers = async () => {
const { setSendTo, setInfo, setRescanning, history, openErrorModal, openPasswordAndUnlockIfNeeded } = this.props;
// About
ipcRenderer.on('about', () => {
openErrorModal(
'Zecwallet Lite',
<div className={cstyles.verticalflex}>
<div className={cstyles.margintoplarge}>Zecwallet Lite v1.1.0-beta1</div>
<div className={cstyles.margintoplarge}>Built with Electron. Copyright (c) 2018-2020, Aditya Kulkarni.</div>
<div className={cstyles.margintoplarge}>
The MIT License (MIT) Copyright (c) 2018-2020 Zecwallet
<br />
<br />
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
documentation files (the &quot;Software&quot;), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
following conditions:
<br />
<br />
The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.
<br />
<br />
THE SOFTWARE IS PROVIDED &quot;AS IS&quot;, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
USE OR OTHER DEALINGS IN THE SOFTWARE.
</div>
</div>
);
});
// Donate button
ipcRenderer.on('donate', () => {
const { info } = this.props;
setSendTo(
Utils.getDonationAddress(info.testnet),
Utils.getDefaultDonationAmount(info.testnet),
Utils.getDefaultDonationMemo(info.testnet)
);
history.push(routes.SEND);
});
// Pay URI
ipcRenderer.on('payuri', (event, uri) => {
this.openURIModal(uri);
});
// Export Seed
ipcRenderer.on('seed', () => {
openPasswordAndUnlockIfNeeded(() => {
const seed = RPC.fetchSeed();
openErrorModal(
'Wallet Seed',
<div className={cstyles.verticalflex}>
<div>
This is your wallet&rsquo;s seed phrase. It can be used to recover your entire wallet.
<br />
PLEASE KEEP IT SAFE!
</div>
<hr />
<div style={{ wordBreak: 'break-word', fontFamily: 'monospace, Roboto' }}>{seed}</div>
<hr />
</div>
);
});
});
// Encrypt wallet
ipcRenderer.on('encrypt', async () => {
const { info, lockWallet, encryptWallet, openPassword } = this.props;
if (info.encrypted && info.locked) {
openErrorModal('Already Encrypted', 'Your wallet is already encrypted and locked.');
} else if (info.encrypted && !info.locked) {
await lockWallet();
openErrorModal('Locked', 'Your wallet has been locked. A password will be needed to spend funds.');
} else {
// Encrypt the wallet
openPassword(
true,
async password => {
await encryptWallet(password);
openErrorModal('Encrypted', 'Your wallet has been encrypted. The password will be needed to spend funds.');
},
() => {
openErrorModal('Cancelled', 'Your wallet was not encrypted.');
},
<div>
Please enter a password to encrypt your wallet. <br />
WARNING: If you forget this password, the only way to recover your wallet is from the seed phrase.
</div>
);
}
});
// Remove wallet encryption
ipcRenderer.on('decrypt', async () => {
const { info, decryptWallet, openPassword } = this.props;
if (!info.encrypted) {
openErrorModal('Not Encrypted', 'Your wallet is not encrypted and ready for spending.');
} else {
// Remove the wallet remove the wallet encryption
openPassword(
false,
async password => {
const success = await decryptWallet(password);
if (success) {
openErrorModal(
'Decrypted',
`Your wallet's encryption has been removed. A password will no longer be needed to spend funds.`
);
} else {
openErrorModal('Decryption Failed', 'Wallet decryption failed. Do you have the right password?');
}
},
() => {
openErrorModal('Cancelled', 'Your wallet is still encrypted.');
}
);
}
});
// Unlock wallet
ipcRenderer.on('unlock', () => {
const { info } = this.props;
if (!info.encrypted || !info.locked) {
openErrorModal('Already Unlocked', 'Your wallet is already unlocked for spending');
} else {
openPasswordAndUnlockIfNeeded(async () => {
openErrorModal('Unlocked', 'Your wallet is unlocked for spending');
});
}
});
// Rescan
ipcRenderer.on('rescan', () => {
// To rescan, we reset the wallet loading
// So set info the default, and redirect to the loading screen
RPC.doRescan();
// Set the rescanning global state to true
setRescanning(true);
// Reset the info object, it will be refetched
setInfo(new Info());
history.push(routes.LOADING);
});
// Export all private keys
ipcRenderer.on('exportall', async () => {
// Get all the addresses and run export key on each of them.
const { addresses, getPrivKeyAsString } = this.props;
openPasswordAndUnlockIfNeeded(async () => {
const privKeysPromise = addresses.map(async a => {
const privKey = await getPrivKeyAsString(a);
return `${privKey} #${a}`;
});
const exportedPrivKeys = await Promise.all(privKeysPromise);
this.setState({ exportPrivKeysModalIsOpen: true, exportedPrivKeys });
});
});
// View zcashd
ipcRenderer.on('zcashd', () => {
history.push(routes.ZCASHD);
});
// Connect mobile app
ipcRenderer.on('connectmobile', () => {
history.push(routes.CONNECTMOBILE);
});
};
closeExportPrivKeysModal = () => {
this.setState({ exportPrivKeysModalIsOpen: false, exportedPrivKeys: null });
};
openURIModal = (defaultValue: string | null) => {
const uriModalInputValue = defaultValue || '';
this.setState({ uriModalIsOpen: true, uriModalInputValue });
};
setURIInputValue = (uriModalInputValue: string) => {
this.setState({ uriModalInputValue });
};
closeURIModal = () => {
this.setState({ uriModalIsOpen: false });
};
payURI = (uri: string) => {
console.log(`Paying ${uri}`);
const { openErrorModal, setSendTo, history } = this.props;
const errTitle = 'URI Error';
const errBody = (
<span>
The URI &quot;{escape(uri)}&quot; was not recognized.
<br />
Please type in a valid URI of the form &quot; zcash:address?amout=xx&memo=yy &quot;
</span>
);
if (!uri || uri === '') {
openErrorModal(errTitle, errBody);
return;
}
const parsedUri = url.parse(uri);
if (!parsedUri || parsedUri.protocol !== 'zcash:' || !parsedUri.query) {
openErrorModal(errTitle, errBody);
return;
}
const address = parsedUri.host;
if (!address || !(Utils.isTransparent(address) || Utils.isZaddr(address))) {
openErrorModal(errTitle, <span>The address ${address} was not recognized as a Zcash address</span>);
return;
}
const parsedParams = querystring.parse(parsedUri.query);
if (!parsedParams || (!parsedParams.amt && !parsedParams.amount)) {
openErrorModal(errTitle, errBody);
return;
}
const amount = parsedParams.amt || parsedParams.amount;
const memo = parsedParams.memo || '';
setSendTo(address, amount, memo);
history.push(routes.SEND);
};
render() {
const { location, info } = this.props;
const { uriModalIsOpen, uriModalInputValue, exportPrivKeysModalIsOpen, exportedPrivKeys } = this.state;
let state = 'DISCONNECTED';
let progress = 100;
if (info && info.version) {
if (info.verificationProgress < 0.9999) {
state = 'SYNCING';
progress = (info.verificationProgress * 100).toFixed(1);
} else {
state = 'CONNECTED';
}
}
return (
<div>
{/* Payment URI Modal */}
<PayURIModal
modalInput={uriModalInputValue}
setModalInput={this.setURIInputValue}
modalIsOpen={uriModalIsOpen}
closeModal={this.closeURIModal}
modalTitle="Pay URI"
actionButtonName="Pay URI"
actionCallback={this.payURI}
/>
{/* Exported (all) Private Keys */}
<ExportPrivKeyModal
modalIsOpen={exportPrivKeysModalIsOpen}
exportedPrivKeys={exportedPrivKeys}
closeModal={this.closeExportPrivKeysModal}
/>
<div className={[cstyles.center, styles.sidebarlogobg].join(' ')}>
<img src={Logo} width="70" alt="logo" />
</div>
<div className={styles.sidebar}>
<SidebarMenuItem
name="Dashboard"
routeName={routes.DASHBOARD}
currentRoute={location.pathname}
iconname="fa-home"
/>
<SidebarMenuItem
name="Send"
routeName={routes.SEND}
currentRoute={location.pathname}
iconname="fa-paper-plane"
/>
<SidebarMenuItem
name="Receive"
routeName={routes.RECEIVE}
currentRoute={location.pathname}
iconname="fa-download"
/>
<SidebarMenuItem
name="Transactions"
routeName={routes.TRANSACTIONS}
currentRoute={location.pathname}
iconname="fa-list"
/>
<SidebarMenuItem
name="Address Book"
routeName={routes.ADDRESSBOOK}
currentRoute={location.pathname}
iconname="fa-address-book"
/>
</div>
<div className={cstyles.center}>
{state === 'CONNECTED' && (
<div className={[cstyles.padsmallall, cstyles.margintopsmall, cstyles.blackbg].join(' ')}>
<i className={[cstyles.green, 'fas', 'fa-check'].join(' ')} />
&nbsp; Connected
</div>
)}
{state === 'SYNCING' && (
<div className={[cstyles.padsmallall, cstyles.margintopsmall, cstyles.blackbg].join(' ')}>
<div>
<i className={[cstyles.yellow, 'fas', 'fa-sync'].join(' ')} />
&nbsp; Syncing
</div>
<div>{`${progress}%`}</div>
</div>
)}
{state === 'DISCONNECTED' && (
<div className={[cstyles.padsmallall, cstyles.margintopsmall, cstyles.blackbg].join(' ')}>
<i className={[cstyles.yellow, 'fas', 'fa-times-circle'].join(' ')} />
&nbsp; Connected
</div>
)}
</div>
</div>
);
}
}
export default withRouter(Sidebar);

View File

@ -0,0 +1,62 @@
.txbox {
margin-left: 16px;
margin-right: 16px;
margin-top: 4px;
margin-bottom: 16px;
display: flex;
cursor: pointer;
}
.txdate {
margin-left: 16px;
}
.txtype {
width: 15%;
float: left;
}
.txaddressamount {
width: 85%;
float: left;
display: flex;
flex-direction: column;
}
.txaddress {
width: 75%;
float: left;
}
.txamount {
width: 20%;
float: right;
}
.txmemo {
word-break: break-all;
}
.txmodal {
top: 12.5%;
left: 12.5%;
right: 12.5%;
bottom: auto;
opacity: 1;
transform: translate(-0%, -12.5%);
background: #212124;
position: absolute;
padding: 16px;
min-width: 700px;
max-height: calc(100vh - 100px);
overflow-y: auto;
}
.txmodalOverlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(100, 100, 100, 0.5);
}

View File

@ -0,0 +1,323 @@
/* eslint-disable react/prop-types */
/* eslint-disable jsx-a11y/click-events-have-key-events */
/* eslint-disable jsx-a11y/no-static-element-interactions */
import React, { Component } from 'react';
import Modal from 'react-modal';
import dateformat from 'dateformat';
import { shell } from 'electron';
import { withRouter } from 'react-router';
import { BalanceBlockHighlight } from './BalanceBlocks';
import styles from './Transactions.css';
import cstyles from './Common.css';
import { Transaction, Info } from './AppState';
import ScrollPane from './ScrollPane';
import Utils from '../utils/utils';
import AddressBook from './Addressbook';
import routes from '../constants/routes.json';
const TxModalInternal = ({ modalIsOpen, tx, closeModal, currencyName, zecPrice, setSendTo, history }) => {
let txid = '';
let type = '';
let typeIcon = '';
let typeColor = '';
let confirmations = 0;
let detailedTxns = [];
let amount = 0;
let datePart = '';
let timePart = '';
if (tx) {
txid = tx.txid;
type = tx.type;
if (tx.type === 'receive') {
typeIcon = 'fa-arrow-circle-down';
typeColor = 'green';
} else {
typeIcon = 'fa-arrow-circle-up';
typeColor = 'red';
}
datePart = dateformat(tx.time * 1000, 'mmm dd, yyyy');
timePart = dateformat(tx.time * 1000, 'hh:MM tt');
confirmations = tx.confirmations;
detailedTxns = tx.detailedTxns;
amount = Math.abs(tx.amount);
}
const openTxid = () => {
if (currencyName === 'TAZ') {
shell.openExternal(`https://chain.so/tx/ZECTEST/${txid}`);
} else {
shell.openExternal(`https://zcha.in/transactions/${txid}`);
}
};
const doReply = (address: string) => {
setSendTo(address, 0.0001, null);
closeModal();
history.push(routes.SEND);
};
return (
<Modal
isOpen={modalIsOpen}
onRequestClose={closeModal}
className={styles.txmodal}
overlayClassName={styles.txmodalOverlay}
>
<div className={[cstyles.verticalflex].join(' ')}>
<div className={[cstyles.marginbottomlarge, cstyles.center].join(' ')}>Transaction Status</div>
<div className={[cstyles.center].join(' ')}>
<i className={['fas', typeIcon].join(' ')} style={{ fontSize: '96px', color: typeColor }} />
</div>
<div className={[cstyles.center].join(' ')}>
{type}
<BalanceBlockHighlight
zecValue={amount}
usdValue={Utils.getZecToUsdString(zecPrice, Math.abs(amount))}
currencyName={currencyName}
/>
</div>
<div className={[cstyles.flexspacebetween].join(' ')}>
<div>
<div className={[cstyles.sublight].join(' ')}>Time</div>
<div>
{datePart} {timePart}
</div>
</div>
<div>
<div className={[cstyles.sublight].join(' ')}>Confirmations</div>
<div>{confirmations}</div>
</div>
</div>
<div className={cstyles.margintoplarge} />
<div className={[cstyles.flexspacebetween].join(' ')}>
<div>
<div className={[cstyles.sublight].join(' ')}>TXID</div>
<div>{txid}</div>
</div>
<div className={cstyles.primarybutton} onClick={openTxid}>
View TXID &nbsp;
<i className={['fas', 'fa-external-link-square-alt'].join(' ')} />
</div>
</div>
<div className={cstyles.margintoplarge} />
<hr />
{detailedTxns.map(txdetail => {
const { bigPart, smallPart } = Utils.splitZecAmountIntoBigSmall(Math.abs(txdetail.amount));
let { address } = txdetail;
const { memo } = txdetail;
if (!address) {
address = '(Shielded)';
}
let replyTo = null;
if (tx.type === 'receive' && memo) {
const split = memo.split(/[ :\n\r\t]+/);
console.log(split);
if (split && split.length > 0 && Utils.isSapling(split[split.length - 1])) {
replyTo = split[split.length - 1];
}
}
console.log('replyto is', replyTo);
return (
<div key={address} className={cstyles.verticalflex}>
<div className={[cstyles.sublight].join(' ')}>Address</div>
<div>{Utils.splitStringIntoChunks(address, 6).join(' ')}</div>
<div className={cstyles.margintoplarge} />
<div className={[cstyles.sublight].join(' ')}>Amount</div>
<div>
<span>
{currencyName} {bigPart}
</span>
<span className={[cstyles.small, cstyles.zecsmallpart].join(' ')}>{smallPart}</span>
</div>
<div className={cstyles.margintoplarge} />
{memo && (
<div>
<div className={[cstyles.sublight].join(' ')}>Memo</div>
<div className={[cstyles.flexspacebetween].join(' ')}>
<div>{memo}</div>
{replyTo && (
<div className={cstyles.primarybutton} onClick={() => doReply(replyTo)}>
Reply
</div>
)}
</div>
</div>
)}
<hr />
</div>
);
})}
<div className={[cstyles.center, cstyles.margintoplarge].join(' ')}>
<button type="button" className={cstyles.primarybutton} onClick={closeModal}>
Close
</button>
</div>
</div>
</Modal>
);
};
const TxModal = withRouter(TxModalInternal);
const TxItemBlock = ({ transaction, currencyName, zecPrice, txClicked, addressBookMap }) => {
const txDate = new Date(transaction.time * 1000);
const datePart = dateformat(txDate, 'mmm dd, yyyy');
const timePart = dateformat(txDate, 'hh:MM tt');
return (
<div>
<div className={[cstyles.small, cstyles.sublight, styles.txdate].join(' ')}>{datePart}</div>
<div
className={[cstyles.well, styles.txbox].join(' ')}
onClick={() => {
txClicked(transaction);
}}
>
<div className={styles.txtype}>
<div>{transaction.type}</div>
<div className={[cstyles.padtopsmall, cstyles.sublight].join(' ')}>{timePart}</div>
</div>
<div className={styles.txaddressamount}>
{transaction.detailedTxns.map(txdetail => {
const { bigPart, smallPart } = Utils.splitZecAmountIntoBigSmall(Math.abs(txdetail.amount));
let { address } = txdetail;
const { memo } = txdetail;
if (!address) {
address = '(Shielded)';
}
const label = addressBookMap[address] || '';
return (
<div key={address} className={cstyles.padtopsmall}>
<div className={styles.txaddress}>
<div className={cstyles.highlight}>{label}</div>
<div>{Utils.splitStringIntoChunks(address, 6).join(' ')}</div>
<div className={[cstyles.small, cstyles.sublight, cstyles.padtopsmall, styles.txmemo].join(' ')}>
{memo}
</div>
</div>
<div className={[styles.txamount, cstyles.right].join(' ')}>
<div>
<span>
{currencyName} {bigPart}
</span>
<span className={[cstyles.small, cstyles.zecsmallpart].join(' ')}>{smallPart}</span>
</div>
<div className={[cstyles.sublight, cstyles.small, cstyles.padtopsmall].join(' ')}>
{Utils.getZecToUsdString(zecPrice, Math.abs(txdetail.amount))}
</div>
</div>
</div>
);
})}
</div>
</div>
</div>
);
};
type Props = {
transactions: Transaction[],
addressBook: AddressBook[],
info: Info,
setSendTo: (string, number | null, string | null) => void
};
type State = {
clickedTx: Transaction | null,
modalIsOpen: boolean
};
export default class Transactions extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { clickedTx: null, modalIsOpen: false };
}
txClicked = (tx: Transaction) => {
// Show the modal
if (!tx) return;
this.setState({ clickedTx: tx, modalIsOpen: true });
};
closeModal = () => {
this.setState({ clickedTx: null, modalIsOpen: false });
};
render() {
const { transactions, info, addressBook, setSendTo } = this.props;
const { clickedTx, modalIsOpen } = this.state;
const addressBookMap = addressBook.reduce((map, obj) => {
// eslint-disable-next-line no-param-reassign
map[obj.address] = obj.label;
return map;
}, {});
return (
<div>
<div className={[cstyles.xlarge, cstyles.padall, cstyles.center].join(' ')}>Transactions</div>
{/* Change the hardcoded height */}
<ScrollPane offsetHeight={100}>
{/* If no transactions, show the "loading..." text */
!transactions && <div className={[cstyles.center, cstyles.margintoplarge].join(' ')}>Loading...</div>}
{transactions && transactions.length === 0 && (
<div className={[cstyles.center, cstyles.margintoplarge].join(' ')}>No Transactions Yet</div>
)}
{transactions &&
transactions.map(t => {
const key = t.type + t.txid + t.address;
return (
<TxItemBlock
key={key}
transaction={t}
currencyName={info.currencyName}
zecPrice={info.zecPrice}
txClicked={this.txClicked}
addressBookMap={addressBookMap}
/>
);
})}
</ScrollPane>
<TxModal
modalIsOpen={modalIsOpen}
tx={clickedTx}
closeModal={this.closeModal}
currencyName={info.currencyName}
zecPrice={info.zecPrice}
setSendTo={setSendTo}
/>
</div>
);
}
}

View File

@ -0,0 +1,20 @@
.wormholeqr {
width: 400px;
}
.qrcodecontainer {
margin: 20px;
text-align: center;
}
.appinfocontainer {
width: 50%;
margin-left: 25%;
margin-top: 20px;
}
.appinfo {
margin-top: 20px;
border: 1px solid grey;
padding: 16px;
}

View File

@ -0,0 +1,110 @@
import React, { PureComponent } from 'react';
import QRCode from 'qrcode.react';
import dateformat from 'dateformat';
import cstyles from './Common.css';
import styles from './WormholeConnection.css';
import CompanionAppListener from '../companion';
import { ConnectedCompanionApp } from './AppState';
type Props = {
companionAppListener: CompanionAppListener,
connectedCompanionApp: ConnectedCompanionApp | null
};
type State = {
tempKeyHex: string
};
export default class WormholeConnection extends PureComponent<Props, State> {
constructor(props) {
super(props);
this.state = { tempKeyHex: null };
}
componentDidMount() {
// If there is no temp key, create one
const { companionAppListener } = this.props;
const { tempKeyHex } = this.state;
if (!tempKeyHex) {
const newKey = companionAppListener.genNewKeyHex();
companionAppListener.createTmpClient(newKey);
this.setState({ tempKeyHex: newKey });
}
}
componentWillUnmount() {
const { companionAppListener } = this.props;
companionAppListener.closeTmpClient();
}
disconnectCurrentMobile = () => {
const { companionAppListener } = this.props;
companionAppListener.disconnectLastClient();
};
render() {
const { tempKeyHex } = this.state;
const { connectedCompanionApp } = this.props;
const clientName = (connectedCompanionApp && connectedCompanionApp.name) || null;
const lastSeen = (connectedCompanionApp && connectedCompanionApp.lastSeen) || null;
let datePart = null;
let timePart = null;
if (lastSeen) {
const txDate = new Date(lastSeen);
datePart = dateformat(txDate, 'mmm dd, yyyy');
timePart = dateformat(txDate, 'hh:MM tt');
}
const connStr = `ws://127.0.0.1:7070,${tempKeyHex},1`;
return (
<div>
<div className={[cstyles.xlarge, cstyles.padall, cstyles.center].join(' ')}>Connect Mobile App</div>
<div className={styles.qrcodecontainer}>
<div>This is your connection code. Scan this QR code from the Zecwallet Companion App.</div>
<div className={[cstyles.center, cstyles.margintoplarge].join(' ')}>
<QRCode value={connStr} size={256} className={styles.wormholeqr} />
</div>
<div className={[cstyles.sublight, cstyles.margintoplarge, cstyles.small].join(' ')}>{connStr}</div>
</div>
<div className={styles.appinfocontainer}>
<div className={styles.appinfo}>
{clientName && (
<div>
<div className={cstyles.flexspacebetween}>
<div style={{ flex: 1 }} className={cstyles.sublight}>
Current App Connected:
</div>
<div style={{ flex: 1 }}>{clientName}</div>
</div>
<div style={{ display: 'flex' }}>
<div style={{ flex: 1 }} className={cstyles.sublight}>
Last Seen:
</div>
<div style={{ flex: 1 }}>
{datePart} {timePart}
</div>
</div>
<div className={cstyles.margintoplarge}>
<button type="button" className={cstyles.primarybutton} onClick={this.disconnectCurrentMobile}>
Disconnect
</button>
</div>
</div>
)}
{!clientName && <div>No Companion App Connected</div>}
</div>
</div>
</div>
);
}
}

33
app/components/Zcashd.css Normal file
View File

@ -0,0 +1,33 @@
.container {
overflow: hidden;
display: flex;
flex-direction: column;
}
.imgcontainer {
width: 100%;
text-align: center;
margin-top: 10%;
}
.imgcontainer img {
max-width: 500px;
}
.detailcontainer {
width: 50%;
flex-direction: column;
margin-left: 25%;
margin-top: 5%;
}
.detaillines {
border: 1px solid grey;
padding: 16px;
}
.detailline {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
}

77
app/components/Zcashd.js Normal file
View File

@ -0,0 +1,77 @@
/* eslint-disable react/prop-types */
import React, { Component } from 'react';
import { Info } from './AppState';
import cstyles from './Common.css';
import styles from './Zcashd.css';
import ScrollPane from './ScrollPane';
import Heart from '../assets/img/zcashdlogo.gif';
const DetailLine = ({ label, value }) => {
return (
<div className={styles.detailline}>
<div className={cstyles.sublight}>{label} :</div>
<div>{value}</div>
</div>
);
};
type Props = {
info: Info,
refresh: PropTypes.object.isRequired
};
export default class Zcashd extends Component<Props> {
render() {
const { info, rpcConfig, refresh } = this.props;
const { url } = rpcConfig;
if (!info || !info.version) {
return (
<div>
<div className={[cstyles.verticalflex, cstyles.center].join(' ')}>
<div style={{ marginTop: '100px' }}>
<i className={['fas', 'fa-times-circle'].join(' ')} style={{ fontSize: '96px', color: 'red' }} />
</div>
<div className={cstyles.margintoplarge}>Not Connected</div>
</div>
</div>
);
// eslint-disable-next-line no-else-return
} else {
let height = info.latestBlock;
if (info.verificationProgress < 0.9999) {
const progress = (info.verificationProgress * 100).toFixed(1);
height = `${height} (${progress}%)`;
}
return (
<div>
<div className={styles.container}>
<ScrollPane offsetHeight={0}>
<div className={styles.imgcontainer}>
<img src={Heart} alt="heart" />
</div>
<div className={styles.detailcontainer}>
<div className={styles.detaillines}>
<DetailLine label="version" value={info.version} />
<DetailLine label="Lightwallet Server" value={url} />
<DetailLine label="Network" value={info.testnet ? 'Testnet' : 'Mainnet'} />
<DetailLine label="Block Height" value={height} />
</div>
</div>
<div className={cstyles.buttoncontainer}>
<button className={cstyles.primarybutton} type="button" onClick={refresh}>
Refresh All Data
</button>
</div>
<div className={cstyles.margintoplarge} />
</ScrollPane>
</div>
</div>
);
}
}
}

11
app/constants/routes.json Normal file
View File

@ -0,0 +1,11 @@
{
"LOADING": "/",
"DASHBOARD": "/dashboard",
"SEND": "/send",
"RECEIVE": "/receive",
"ADDRESSBOOK": "/addressbook",
"TRANSACTIONS": "/transactions",
"SETTINGS": "/settings",
"ZCASHD": "/zcashd",
"CONNECTMOBILE": "/connectmobile"
}

15
app/containers/App.js Normal file
View File

@ -0,0 +1,15 @@
// @flow
import * as React from 'react';
type Props = {
children: React.Node
};
export default class App extends React.Component<Props> {
props: Props;
render() {
const { children } = this.props;
return <>{children}</>;
}
}

13
app/containers/Root.js Normal file
View File

@ -0,0 +1,13 @@
// @flow
import React from 'react';
import { BrowserRouter as Router } from 'react-router-dom';
import { hot } from 'react-hot-loader/root';
import Routes from '../Routes';
const Root = () => (
<Router>
<Routes />
</Router>
);
export default hot(Root);

14
app/index.js Normal file
View File

@ -0,0 +1,14 @@
import React, { Fragment } from 'react';
import { render } from 'react-dom';
import { AppContainer as ReactHotAppContainer } from 'react-hot-loader';
import Root from './containers/Root';
import './app.global.css';
const AppContainer = process.env.PLAIN_HMR ? Fragment : ReactHotAppContainer;
render(
<AppContainer>
<Root />
</AppContainer>,
document.getElementById('root')
);

152
app/main.dev.js Normal file
View File

@ -0,0 +1,152 @@
/* eslint-disable compat/compat */
/* eslint global-require: off */
/**
* This module executes inside of electron's main process. You can start
* electron renderer process from here and communicate with the other processes
* through IPC.
*
* When running `yarn build` or `yarn build-main`, this file is compiled to
* `./app/main.prod.js` using webpack. This gives us some performance wins.
*
* @flow
*/
import { app, shell, BrowserWindow, ipcMain } from 'electron';
import log from 'electron-log';
import MenuBuilder from './menu';
export default class AppUpdater {
constructor() {
log.transports.file.level = 'info';
}
}
let mainWindow = null;
if (process.env.NODE_ENV === 'production') {
const sourceMapSupport = require('source-map-support');
sourceMapSupport.install();
}
if (process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true') {
require('electron-debug')();
}
const installExtensions = async () => {
const installer = require('electron-devtools-installer');
const forceDownload = !!process.env.UPGRADE_EXTENSIONS;
const extensions = ['REACT_DEVELOPER_TOOLS', 'REDUX_DEVTOOLS'];
return Promise.all(extensions.map(name => installer.default(installer[name], forceDownload))).catch(console.log);
};
let waitingForClose = false;
let proceedToClose = false;
const createWindow = async () => {
if (process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true') {
await installExtensions();
}
mainWindow = new BrowserWindow({
show: false,
width: 1300,
height: 728,
minHeight: 500,
minWidth: 1100,
webPreferences: {
// Allow node integration because we're only loading local content here.
nodeIntegration: true
}
});
mainWindow.loadURL(`file://${__dirname}/app.html`);
app.on('web-contents-created', (event, contents) => {
contents.on('new-window', async (eventInner, navigationUrl) => {
// In this example, we'll ask the operating system
// to open this event's url in the default browser.
console.log('attempting to open window', navigationUrl);
eventInner.preventDefault();
await shell.openExternal(navigationUrl);
});
});
// @TODO: Use 'ready-to-show' event
// https://github.com/electron/electron/blob/master/docs/api/browser-window.md#using-ready-to-show-event
mainWindow.webContents.on('did-finish-load', () => {
if (!mainWindow) {
throw new Error('"mainWindow" is not defined');
}
if (process.env.START_MINIMIZED) {
mainWindow.minimize();
} else {
mainWindow.show();
mainWindow.focus();
}
});
mainWindow.on('close', event => {
// If we are clear to close, then return and allow everything to close
if (proceedToClose) {
return;
}
// If we're already waiting for close, then don't allow another close event to actually close the window
if (waitingForClose) {
console.log('Waiting for close... Timeout in 10s');
event.preventDefault();
return;
}
waitingForClose = true;
event.preventDefault();
ipcMain.on('appquitdone', () => {
waitingForClose = false;
proceedToClose = true;
app.quit();
});
// $FlowFixMe
mainWindow.webContents.send('appquitting');
// Failsafe, timeout after 3 seconds
setTimeout(() => {
waitingForClose = false;
proceedToClose = true;
console.log('Timeout, quitting');
app.quit();
}, 3 * 1000);
});
mainWindow.on('closed', () => {
mainWindow = null;
});
const menuBuilder = new MenuBuilder(mainWindow);
menuBuilder.buildMenu();
// Remove this if your app does not use auto updates
// eslint-disable-next-line
new AppUpdater();
};
/**
* Add event listeners...
*/
app.on('window-all-closed', () => {
app.quit();
});
app.on('ready', createWindow);
app.on('activate', () => {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (mainWindow === null) createWindow();
});

1
app/main.prod.js.LICENSE Normal file
View File

@ -0,0 +1 @@
/*! http://mths.be/fromcodepoint v0.1.0 by @mathias */

367
app/menu.js Normal file
View File

@ -0,0 +1,367 @@
// @flow
import { app, Menu, shell, BrowserWindow } from 'electron';
export default class MenuBuilder {
mainWindow: BrowserWindow;
constructor(mainWindow: BrowserWindow) {
this.mainWindow = mainWindow;
}
buildMenu() {
if (process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true') {
this.setupDevelopmentEnvironment();
}
const template = process.platform === 'darwin' ? this.buildDarwinTemplate() : this.buildDefaultTemplate();
const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
return menu;
}
setupDevelopmentEnvironment() {
this.mainWindow.openDevTools();
this.mainWindow.webContents.on('context-menu', (e, props) => {
const { x, y } = props;
Menu.buildFromTemplate([
{
label: 'Inspect element',
click: () => {
this.mainWindow.inspectElement(x, y);
}
}
]).popup(this.mainWindow);
});
}
buildDarwinTemplate() {
const { mainWindow } = this;
const subMenuAbout = {
label: 'Zecwallet Lite',
submenu: [
{
label: 'About Zecwallet Lite',
selector: 'orderFrontStandardAboutPanel:',
click: () => {
mainWindow.webContents.send('about');
}
},
{ type: 'separator' },
{ label: 'Services', submenu: [] },
{ type: 'separator' },
{
label: 'Hide Zecwallet Lite',
accelerator: 'Command+H',
selector: 'hide:'
},
{
label: 'Hide Others',
accelerator: 'Command+Shift+H',
selector: 'hideOtherApplications:'
},
{ label: 'Show All', selector: 'unhideAllApplications:' },
{ type: 'separator' },
{
label: 'Quit',
accelerator: 'Command+Q',
click: () => {
app.quit();
}
}
]
};
const subMenuEdit = {
label: 'Edit',
submenu: [
{ label: 'Undo', accelerator: 'Command+Z', selector: 'undo:' },
{ label: 'Redo', accelerator: 'Shift+Command+Z', selector: 'redo:' },
{ type: 'separator' },
{ label: 'Cut', accelerator: 'Command+X', selector: 'cut:' },
{ label: 'Copy', accelerator: 'Command+C', selector: 'copy:' },
{ label: 'Paste', accelerator: 'Command+V', selector: 'paste:' },
{
label: 'Select All',
accelerator: 'Command+A',
selector: 'selectAll:'
}
]
};
const subMenuViewDev = {
label: 'Wallet',
submenu: [
{
label: 'Wallet Seed',
click: () => {
mainWindow.webContents.send('seed');
}
},
{
label: '&Export All Private Keys',
click: () => {
mainWindow.webContents.send('exportall');
}
},
{
label: '&Rescan',
click: () => {
mainWindow.webContents.send('rescan');
}
},
{
label: 'View Lightwalletd Info',
click: () => {
this.mainWindow.webContents.send('zcashd');
}
},
{
label: 'Connect Mobile App',
click: () => {
this.mainWindow.webContents.send('connectmobile');
}
},
{ type: 'separator' },
{
label: 'Encrypt Wallet',
click: () => {
this.mainWindow.webContents.send('encrypt');
}
},
{
label: 'Remove Wallet Encryption',
click: () => {
this.mainWindow.webContents.send('decrypt');
}
},
{
label: 'Unlock',
click: () => {
this.mainWindow.webContents.send('unlock');
}
}
// { type: 'separator' },
// {
// label: 'Toggle Developer Tools',
// accelerator: 'Alt+Command+I',
// click: () => {
// this.mainWindow.toggleDevTools();
// }
// }
]
};
const subMenuViewProd = {
label: 'Wallet',
submenu: [
{
label: 'Wallet Seed',
click: () => {
mainWindow.webContents.send('seed');
}
},
{
label: '&Export All Private Keys',
click: () => {
mainWindow.webContents.send('exportall');
}
},
{
label: '&Rescan',
click: () => {
mainWindow.webContents.send('rescan');
}
},
{
label: 'Server info',
click: () => {
this.mainWindow.webContents.send('zcashd');
}
},
{
label: 'Connect Mobile App',
click: () => {
this.mainWindow.webContents.send('connectmobile');
}
},
{ type: 'separator' },
{
label: 'Encrypt Wallet',
click: () => {
this.mainWindow.webContents.send('encrypt');
}
},
{
label: 'Remove Wallet Encryption',
click: () => {
this.mainWindow.webContents.send('decrypt');
}
},
{
label: 'Unlock',
click: () => {
this.mainWindow.webContents.send('unlock');
}
}
]
};
const subMenuWindow = {
label: 'Window',
submenu: [
{
label: 'Minimize',
accelerator: 'Command+M',
selector: 'performMiniaturize:'
},
{ label: 'Close', accelerator: 'Command+W', selector: 'performClose:' },
{ type: 'separator' },
{ label: 'Bring All to Front', selector: 'arrangeInFront:' }
]
};
const subMenuHelp = {
label: 'Help',
submenu: [
{
label: 'Donate',
click() {
mainWindow.webContents.send('donate');
}
},
{
label: 'Check github.com for updates',
click() {
shell.openExternal('https://github.com/adityapk00/zecwallet-lite/releases');
}
},
{
label: 'File a bug...',
click() {
shell.openExternal('https://github.com/adityapk00/zecwallet-lite/issues');
}
}
]
};
const subMenuView = process.env.NODE_ENV === 'development' ? subMenuViewDev : subMenuViewProd;
return [subMenuAbout, subMenuEdit, subMenuView, subMenuWindow, subMenuHelp];
}
buildDefaultTemplate() {
const { mainWindow } = this;
const templateDefault = [
{
label: '&File',
submenu: [
{
label: '&Pay URI',
accelerator: 'Ctrl+P',
click: () => {
mainWindow.webContents.send('payuri');
}
},
{
label: '&Close',
accelerator: 'Ctrl+W',
click: () => {
this.mainWindow.close();
}
}
]
},
{
label: '&Wallet',
submenu: [
{
label: 'Wallet Seed',
click: () => {
mainWindow.webContents.send('seed');
}
},
{
label: '&Export All Private Keys',
click: () => {
mainWindow.webContents.send('exportall');
}
},
{
label: '&Rescan',
click: () => {
mainWindow.webContents.send('rescan');
}
},
{
label: 'Server info',
click: () => {
this.mainWindow.webContents.send('zcashd');
}
},
{
label: 'Connect Mobile App',
click: () => {
this.mainWindow.webContents.send('connectmobile');
}
},
// {
// label: 'Devtools',
// click: () => {
// mainWindow.webContents.openDevTools();
// }
// },
{ type: 'separator' },
{
label: 'Encrypt Wallet',
click: () => {
this.mainWindow.webContents.send('encrypt');
}
},
{
label: 'Remove Wallet Encryption',
click: () => {
this.mainWindow.webContents.send('decrypt');
}
},
{
label: 'Unlock',
click: () => {
this.mainWindow.webContents.send('unlock');
}
}
]
},
{
label: 'Help',
submenu: [
{
label: 'About Zecwallet Lite',
click: () => {
mainWindow.webContents.send('about');
}
},
{
label: 'Donate',
click() {
mainWindow.webContents.send('donate');
}
},
{
label: 'Check github.com for updates',
click() {
shell.openExternal('https://github.com/adityapk00/zecwallet-lite/releases');
}
},
{
label: 'File a bug...',
click() {
shell.openExternal('https://github.com/adityapk00/zecwallet-lite/issues');
}
}
]
}
];
return templateDefault;
}
}

5
app/package-lock.json generated Normal file
View File

@ -0,0 +1,5 @@
{
"name": "electron-react-boilerplate",
"version": "0.18.1",
"lockfileVersion": 1
}

18
app/package.json Normal file
View File

@ -0,0 +1,18 @@
{
"name": "zecwallet",
"productName": "Zecwallet Lite",
"version": "1.1.0-beta1",
"description": "Zecwallet Lite",
"main": "./main.prod.js",
"author": {
"name": "Aditya Kulkarni",
"email": "aditya@zecwallet.co",
"url": "https://github.com/adityapk00/zecwallet-electron"
},
"scripts": {
"electron-rebuild": "node -r ../internals/scripts/BabelRegister.js ../internals/scripts/ElectronRebuild.js",
"postinstall": "yarn electron-rebuild"
},
"license": "MIT",
"dependencies": {}
}

379
app/rpc.js Normal file
View File

@ -0,0 +1,379 @@
/* eslint-disable max-classes-per-file */
import axios from 'axios';
import { TotalBalance, AddressBalance, Transaction, RPCConfig, TxDetail, Info } from './components/AppState';
import native from '../native/index.node';
export default class RPC {
rpcConfig: RPCConfig;
fnSetInfo: Info => void;
fnSetTotalBalance: TotalBalance => void;
fnSetAddressesWithBalance: (AddressBalance[]) => void;
fnSetTransactionsList: (Transaction[]) => void;
fnSetAllAddresses: (string[]) => void;
fnSetZecPrice: number => void;
refreshTimerID: TimerID;
priceTimerID: TimerID;
constructor(
fnSetTotalBalance: TotalBalance => void,
fnSetAddressesWithBalance: (AddressBalance[]) => void,
fnSetTransactionsList: (Transaction[]) => void,
fnSetAllAddresses: (string[]) => void,
fnSetInfo: Info => void,
fnSetZecPrice: number => void
) {
this.fnSetTotalBalance = fnSetTotalBalance;
this.fnSetAddressesWithBalance = fnSetAddressesWithBalance;
this.fnSetTransactionsList = fnSetTransactionsList;
this.fnSetAllAddresses = fnSetAllAddresses;
this.fnSetInfo = fnSetInfo;
this.fnSetZecPrice = fnSetZecPrice;
}
async configure(rpcConfig: RPCConfig) {
this.rpcConfig = rpcConfig;
if (!this.refreshTimerID) {
this.refreshTimerID = setTimeout(() => this.refresh(), 1000);
}
if (!this.priceTimerID) {
this.priceTimerID = setTimeout(() => this.getZecPrice(), 1000);
}
}
setupNextFetch(lastBlockHeight: number) {
this.refreshTimerID = setTimeout(() => this.refresh(lastBlockHeight), 60 * 1000);
}
static doSync() {
const syncstr = native.litelib_execute('sync', '');
console.log(`Sync exec result: ${syncstr}`);
}
static doRescan() {
const syncstr = native.litelib_execute('rescan', '');
console.log(`rescan exec result: ${syncstr}`);
}
static doSyncStatus(): string {
const syncstr = native.litelib_execute('syncstatus', '');
console.log(`syncstatus: ${syncstr}`);
return syncstr;
}
static doSave() {
const savestr = native.litelib_execute('save', '');
console.log(`Sync status: ${savestr}`);
}
async refresh(lastBlockHeight: number) {
const latestBlockHeight = await this.fetchInfo();
if (!lastBlockHeight || lastBlockHeight < latestBlockHeight) {
// If the latest block height has changed, make sure to sync
await RPC.doSync();
const balP = this.fetchTotalBalance();
const txns = this.fetchTandZTransactions(latestBlockHeight);
await balP;
await txns;
// All done, set up next fetch
console.log(`Finished full refresh at ${latestBlockHeight}`);
} else {
// Still at the latest block
console.log('Already have latest block, waiting for next refresh');
}
this.setupNextFetch(latestBlockHeight);
}
// Special method to get the Info object. This is used both internally and by the Loading screen
static getInfoObject() {
const infostr = native.litelib_execute('info', '');
const infoJSON = JSON.parse(infostr);
const info = new Info();
info.testnet = infoJSON.chain_name === 'test';
info.latestBlock = infoJSON.latest_block_height;
info.connections = 1;
info.version = infoJSON.version;
info.verificationProgress = 1;
info.currencyName = info.testnet ? 'TAZ' : 'ZEC';
info.solps = 0;
const encStatus = native.litelib_execute('encryptionstatus', '');
const encJSON = JSON.parse(encStatus);
info.encrypted = encJSON.encrypted;
info.locked = encJSON.locked;
return info;
}
async fetchInfo(): number {
const info = RPC.getInfoObject(this.rpcConfig);
this.fnSetInfo(info);
return info.latestBlock;
}
// This method will get the total balances
async fetchTotalBalance() {
const balanceStr = native.litelib_execute('balance', '');
const balanceJSON = JSON.parse(balanceStr);
// Total Balance
const balance = new TotalBalance();
balance.private = balanceJSON.zbalance / 10 ** 8;
balance.transparent = balanceJSON.tbalance / 10 ** 8;
balance.verifiedPrivate = balanceJSON.verified_zbalance / 10 ** 8;
balance.total = balance.private + balance.transparent;
this.fnSetTotalBalance(balance);
// Fetch pending notes and UTXOs
const pendingNotes = native.litelib_execute('notes', '');
const pendingJSON = JSON.parse(pendingNotes);
const pendingAddressBalances = new Map();
// Process sapling notes
pendingJSON.pending_notes.forEach(s => {
pendingAddressBalances.set(s.address, s.value);
});
// Process UTXOs
pendingJSON.pending_utxos.forEach(s => {
pendingAddressBalances.set(s.address, s.value);
});
// Addresses with Balance. The lite client reports balances in zatoshi, so divide by 10^8;
const zaddresses = balanceJSON.z_addresses
.map(o => {
// If this has any unconfirmed txns, show that in the UI
const ab = new AddressBalance(o.address, o.zbalance / 10 ** 8);
if (pendingAddressBalances.has(ab.address)) {
ab.containsPending = true;
}
return ab;
})
.filter(ab => ab.balance > 0);
const taddresses = balanceJSON.t_addresses
.map(o => {
// If this has any unconfirmed txns, show that in the UI
const ab = new AddressBalance(o.address, o.balance / 10 ** 8);
if (pendingAddressBalances.has(ab.address)) {
ab.containsPending = true;
}
return ab;
})
.filter(ab => ab.balance > 0);
const addresses = zaddresses.concat(taddresses);
this.fnSetAddressesWithBalance(addresses);
// Also set all addresses
const allZAddresses = balanceJSON.z_addresses.map(o => o.address);
const allTAddresses = balanceJSON.t_addresses.map(o => o.address);
const allAddresses = allZAddresses.concat(allTAddresses);
this.fnSetAllAddresses(allAddresses);
}
static getPrivKeyAsString(address: string): string {
const privKeyStr = native.litelib_execute('export', address);
const privKeyJSON = JSON.parse(privKeyStr);
return privKeyJSON[0].private_key;
}
static createNewAddress(zaddress: boolean) {
const addrStr = native.litelib_execute('new', zaddress ? 'z' : 't');
const addrJSON = JSON.parse(addrStr);
return addrJSON[0];
}
static fetchSeed(): string {
const seedStr = native.litelib_execute('seed', '');
const seedJSON = JSON.parse(seedStr);
return seedJSON.seed;
}
// Fetch all T and Z transactions
async fetchTandZTransactions(latestBlockHeight: number) {
const listStr = native.litelib_execute('list', '');
const listJSON = JSON.parse(listStr);
const txlist = listJSON.map(tx => {
const transaction = new Transaction();
const type = tx.outgoing_metadata ? 'sent' : 'receive';
transaction.address =
// eslint-disable-next-line no-nested-ternary
type === 'sent' ? (tx.outgoing_metadata.length > 0 ? tx.outgoing_metadata[0].address : '') : tx.address;
transaction.type = type;
transaction.amount = tx.amount / 10 ** 8;
transaction.confirmations = latestBlockHeight - tx.block_height + 1;
transaction.txid = tx.txid;
transaction.time = tx.datetime;
if (tx.outgoing_metadata) {
transaction.detailedTxns = tx.outgoing_metadata.map(o => {
const detail = new TxDetail();
detail.address = o.address;
detail.amount = o.value / 10 ** 8;
detail.memo = o.memo;
return detail;
});
} else {
transaction.detailedTxns = [new TxDetail()];
transaction.detailedTxns[0].address = tx.address;
transaction.detailedTxns[0].amount = tx.amount / 10 ** 8;
transaction.detailedTxns[0].memo = tx.memo;
}
return transaction;
});
txlist.sort((t1, t2) => t1.confirmations - t2.confirmations);
this.fnSetTransactionsList(txlist);
}
// Send a transaction using the already constructed sendJson structure
sendTransaction(sendJson: []): string {
let sendStr;
try {
sendStr = native.litelib_execute('send', JSON.stringify(sendJson));
} catch (err) {
// TODO Show a modal with the error
console.log(`Error sending Tx: ${err}`);
throw err;
}
if (sendStr.startsWith('Error')) {
// Throw the proper error
throw sendStr.split(/[\r\n]+/)[0];
}
console.log(`Send response: ${sendStr}`);
const sendJSON = JSON.parse(sendStr);
const { txid, error } = sendJSON;
if (error) {
console.log(`Error sending Tx: ${error}`);
throw error;
} else {
// And refresh data (full refresh)
this.refresh(null);
return txid;
}
}
async encryptWallet(password): boolean {
const resultStr = native.litelib_execute('encrypt', password);
const resultJSON = JSON.parse(resultStr);
// To update the wallet encryption status
this.fetchInfo();
// And save the wallet
RPC.doSave();
return resultJSON.result === 'success';
}
async decryptWallet(password): boolean {
const resultStr = native.litelib_execute('decrypt', password);
const resultJSON = JSON.parse(resultStr);
// To update the wallet encryption status
this.fetchInfo();
// And save the wallet
RPC.doSave();
return resultJSON.result === 'success';
}
async lockWallet(): boolean {
const resultStr = native.litelib_execute('lock', '');
const resultJSON = JSON.parse(resultStr);
// To update the wallet encryption status
this.fetchInfo();
return resultJSON.result === 'success';
}
async unlockWallet(password: string): boolean {
const resultStr = native.litelib_execute('unlock', password);
const resultJSON = JSON.parse(resultStr);
// To update the wallet encryption status
this.fetchInfo();
return resultJSON.result === 'success';
}
setupNextZecPriceRefresh(retryCount: number, timeout: number) {
// Every hour
this.priceTimerID = setTimeout(() => this.getZecPrice(retryCount), timeout);
}
async getZecPrice(retryCount: number) {
if (!retryCount) {
// eslint-disable-next-line no-param-reassign
retryCount = 0;
}
try {
const response = await new Promise((resolve, reject) => {
axios('https://api.coinmarketcap.com/v1/ticker/', {
method: 'GET'
})
.then(r => resolve(r.data))
.catch(err => {
reject(err);
});
});
const zecData = response.find(i => i.symbol.toUpperCase() === 'ZEC');
if (zecData) {
this.fnSetZecPrice(zecData.price_usd);
this.setupNextZecPriceRefresh(0, 1000 * 60 * 60); // Every hour
} else {
this.fnSetZecPrice(null);
let timeout = 1000 * 60; // 1 minute
if (retryCount > 5) {
timeout = 1000 * 60 * 60; // an hour later
}
this.setupNextZecPriceRefresh(retryCount + 1, timeout);
}
} catch (err) {
console.log(err);
this.fnSetZecPrice(null);
let timeout = 1000 * 60; // 1 minute
if (retryCount > 5) {
timeout = 1000 * 60 * 60; // an hour later
}
this.setupNextZecPriceRefresh(retryCount + 1, timeout);
}
}
}

0
app/utils/.gitkeep Normal file
View File

View File

@ -0,0 +1,37 @@
import fs from 'fs';
import path from 'path';
import { remote } from 'electron';
import { AddressBookEntry } from '../components/AppState';
// Utility class to save / read the address book.
export default class AddressbookImpl {
static async getFileName() {
const dir = path.join(remote.app.getPath('appData'), 'zecwallet');
if (!fs.existsSync(dir)) {
await fs.promises.mkdir(dir);
}
const fileName = path.join(dir, 'AddressBook.json');
return fileName;
}
// Write the address book to disk
static async writeAddressBook(ab: AddressBookEntry[]) {
const fileName = await this.getFileName();
await fs.promises.writeFile(fileName, JSON.stringify(ab));
}
// Read the address book
static async readAddressBook(): AddressBookEntry[] {
const fileName = await this.getFileName();
try {
return JSON.parse(await fs.promises.readFile(fileName));
} catch (err) {
// File probably doesn't exist, so return nothing
console.log(err);
return [];
}
}
}

50
app/utils/SentTxStore.js Normal file
View File

@ -0,0 +1,50 @@
import os from 'os';
import path from 'path';
import fs from 'fs';
import { remote } from 'electron';
import { Transaction, TxDetail } from '../components/AppState';
export default class SentTxStore {
static locateSentTxStore() {
if (os.platform() === 'darwin') {
return path.join(remote.app.getPath('appData'), 'Zcash', 'senttxstore.dat');
}
if (os.platform() === 'linux') {
return path.join(
remote.app.getPath('home'),
'.local',
'share',
'zec-qt-wallet-org',
'zec-qt-wallet',
'senttxstore.dat'
);
}
return path.join(remote.app.getPath('appData'), 'Zcash', 'senttxstore.dat');
}
static async loadSentTxns(): Transaction[] {
try {
const sentTx = JSON.parse(await fs.promises.readFile(SentTxStore.locateSentTxStore()));
return sentTx.map(s => {
const transction = new Transaction();
transction.type = s.type;
transction.amount = s.amount;
transction.address = s.from;
transction.txid = s.txid;
transction.time = s.datetime;
transction.detailedTxns = [new TxDetail()];
transction.detailedTxns[0].address = s.address;
transction.detailedTxns[0].amount = s.amount;
transction.detailedTxns[0].memo = s.memo;
return transction;
});
} catch (err) {
// If error for whatever reason (most likely, file not found), just return an empty array
return [];
}
}
}

112
app/utils/utils.js Normal file
View File

@ -0,0 +1,112 @@
/* eslint-disable no-unused-vars */
/* eslint-disable no-else-return */
/* eslint-disable no-plusplus */
export const NO_CONNECTION: string = 'Could not connect to zcashd';
export default class Utils {
static isSapling(addr: string): boolean {
if (!addr) return false;
return new RegExp('^z[a-z0-9]{77}$').test(addr) || new RegExp('^ztestsapling[a-z0-9]{76}$').test(addr);
}
static isSprout(addr: string): boolean {
if (!addr) return false;
return new RegExp('^z[a-zA-Z0-9]{94}$').test(addr);
}
static isZaddr(addr: string): boolean {
if (!addr) return false;
return Utils.isSapling(addr) || Utils.isSprout(addr);
}
static isTransparent(addr: string): boolean {
if (!addr) return false;
return new RegExp('^t[a-zA-Z0-9]{34}$').test(addr);
}
// Convert to max 8 decimal places, and remove trailing zeros
static maxPrecision(v: number): string {
if (!v) return v;
if (typeof v === 'string' || v instanceof String) {
// eslint-disable-next-line no-param-reassign
v = parseFloat(v);
}
return v.toFixed(8);
}
static splitZecAmountIntoBigSmall(zecValue: number) {
if (!zecValue) {
return { bigPart: zecValue, smallPart: '' };
}
let bigPart = Utils.maxPrecision(zecValue);
let smallPart = '';
if (bigPart.indexOf('.') >= 0) {
const decimalPart = bigPart.substr(bigPart.indexOf('.') + 1);
if (decimalPart.length > 4) {
smallPart = decimalPart.substr(4);
bigPart = bigPart.substr(0, bigPart.length - smallPart.length);
// Pad the small part with trailing 0s
while (smallPart.length < 4) {
smallPart += '0';
}
}
}
if (smallPart === '0000') {
smallPart = '';
}
return { bigPart, smallPart };
}
static splitStringIntoChunks(s: string, numChunks: number) {
if (numChunks > s.length) return [s];
if (s.length < 16) return [s];
const chunkSize = Math.round(s.length / numChunks);
const chunks = [];
for (let i = 0; i < numChunks - 1; i++) {
chunks.push(s.substr(i * chunkSize, chunkSize));
}
// Last chunk might contain un-even length
chunks.push(s.substr((numChunks - 1) * chunkSize));
return chunks;
}
static nextToAddrID: number = 0;
static getNextToAddrID(): number {
// eslint-disable-next-line no-plusplus
return Utils.nextToAddrID++;
}
static getDonationAddress(testnet: boolean): string {
if (testnet) {
return 'ztestsapling1wn6889vznyu42wzmkakl2effhllhpe4azhu696edg2x6me4kfsnmqwpglaxzs7tmqsq7kudemp5';
} else {
return 'zs1gv64eu0v2wx7raxqxlmj354y9ycznwaau9kduljzczxztvs4qcl00kn2sjxtejvrxnkucw5xx9u';
}
}
static getDefaultDonationAmount(testnet: boolean): number {
return 0.1;
}
static getDefaultDonationMemo(testnet: boolean): string {
return 'Thanks for supporting Zecwallet!';
}
static getZecToUsdString(price: number | null, zecValue: number | null): string {
if (!price || !zecValue) {
return 'USD --';
}
return `USD ${(price * zecValue).toFixed(2)}`;
}
}

4
app/yarn.lock Normal file
View File

@ -0,0 +1,4 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1

View File

@ -1,39 +0,0 @@
<RCC>
<qresource prefix="/fonts">
<file>res/Ubuntu-R.ttf</file>
</qresource>
<qresource prefix="/icons">
<file>res/connected.gif</file>
<file>res/loading.gif</file>
<file>res/paymentreq.gif</file>
<file>res/icon.ico</file>
</qresource>
<qresource prefix="/img">
<file>res/zcashdlogo.gif</file>
<file>res/logobig.gif</file>
</qresource>
<qresource prefix="/translations">
<file>res/zec_qt_wallet_es.qm</file>
<file>res/zec_qt_wallet_fr.qm</file>
<file>res/zec_qt_wallet_pt.qm</file>
<file>res/zec_qt_wallet_it.qm</file>
<file>res/zec_qt_wallet_zh.qm</file>
<file>res/zec_qt_wallet_tr.qm</file>
</qresource>
<qresource prefix="/css">
<file>res/css/blue.css</file>
<file>res/css/dark.css</file>
<file>res/css/default.css</file>
<file>res/css/light.css</file>
</qresource>
<qresource prefix="/images/blue">
<file>res/images/blue/unchecked.png</file>
<file>res/images/blue/checked.png</file>
<file>res/images/blue/blue_downArrow.png</file>
<file>res/images/blue/blue_downArrow_small.png</file>
<file>res/images/blue/blue_upArrow_small.png</file>
<file>res/images/blue/blue_leftArrow_small.png</file>
<file>res/images/blue/blue_rightArrow_small.png</file>
<file>res/images/blue/blue_qtreeview_selected.png</file>
</qresource>
</RCC>

39
appveyor.yml Normal file
View File

@ -0,0 +1,39 @@
image: Visual Studio 2017
platform:
- x64
environment:
matrix:
- nodejs_version: 10
cache:
- '%LOCALAPPDATA%/Yarn'
- node_modules
- app/node_modules
- flow-typed
- '%USERPROFILE%\.electron'
matrix:
fast_finish: true
build: off
version: '{build}'
shallow_clone: true
clone_depth: 1
install:
- ps: Install-Product node $env:nodejs_version x64
- set CI=true
- yarn
test_script:
- yarn package-ci
- yarn lint
# - yarn flow
- yarn test
- yarn build-e2e
- yarn test-e2e

66
babel.config.js Normal file
View File

@ -0,0 +1,66 @@
/* eslint global-require: off */
const developmentEnvironments = ['development', 'test'];
const developmentPlugins = [require('react-hot-loader/babel')];
const productionPlugins = [
require('babel-plugin-dev-expression'),
// babel-preset-react-optimize
require('@babel/plugin-transform-react-constant-elements'),
require('@babel/plugin-transform-react-inline-elements'),
require('babel-plugin-transform-react-remove-prop-types')
];
module.exports = api => {
// see docs about api at https://babeljs.io/docs/en/config-files#apicache
const development = api.env(developmentEnvironments);
return {
presets: [
[
require('@babel/preset-env'),
{
targets: { electron: require('electron/package.json').version }
}
],
require('@babel/preset-flow'),
[require('@babel/preset-react'), { development }]
],
plugins: [
// Stage 0
require('@babel/plugin-proposal-function-bind'),
// Stage 1
require('@babel/plugin-proposal-export-default-from'),
require('@babel/plugin-proposal-logical-assignment-operators'),
[require('@babel/plugin-proposal-optional-chaining'), { loose: false }],
[
require('@babel/plugin-proposal-pipeline-operator'),
{ proposal: 'minimal' }
],
[
require('@babel/plugin-proposal-nullish-coalescing-operator'),
{ loose: false }
],
require('@babel/plugin-proposal-do-expressions'),
// Stage 2
[require('@babel/plugin-proposal-decorators'), { legacy: true }],
require('@babel/plugin-proposal-function-sent'),
require('@babel/plugin-proposal-export-namespace-from'),
require('@babel/plugin-proposal-numeric-separator'),
require('@babel/plugin-proposal-throw-expressions'),
// Stage 3
require('@babel/plugin-syntax-dynamic-import'),
require('@babel/plugin-syntax-import-meta'),
[require('@babel/plugin-proposal-class-properties'), { loose: true }],
require('@babel/plugin-proposal-json-strings'),
...(development ? developmentPlugins : productionPlugins)
]
};
};

1
bin/printversion.ps1 Normal file
View File

@ -0,0 +1 @@
echo "::set-env name=VERSION::1.1.0-beta1"

4
bin/printversion.sh Executable file
View File

@ -0,0 +1,4 @@
#!/bin/bash
VERSION="1.1.0-beta1"
echo "::set-env name=VERSION::$VERSION"

47
bin/signbinaries.sh Executable file
View File

@ -0,0 +1,47 @@
#!/bin/bash
# Accept the variables as command line arguments as well
POSITIONAL=()
while [[ $# -gt 0 ]]
do
key="$1"
case $key in
-v|--version)
APP_VERSION="$2"
shift # past argument
shift # past value
;;
*) # unknown option
POSITIONAL+=("$1") # save it in an array for later
shift # past argument
;;
esac
done
set -- "${POSITIONAL[@]}" # restore positional parameters
if [ -z $APP_VERSION ]; then echo "APP_VERSION is not set"; exit 1; fi
# Store the hash and signatures here
cd release
rm -rf signatures
mkdir signatures
# Remove previous signatures/hashes
rm -f sha256sum-$APP_VERSION.txt
rm -f signatures-$APP_VERSION.zip
# sha256sum the binaries
sha256sum Zecwallet*$APP_VERSION* > sha256sum-$APP_VERSION.txt
OIFS="$IFS"
IFS=$'\n'
for i in `find ./ -iname "Zecwallet*$APP_VERSION*" -o -iname "sha256sum-$APP_VERSION.txt"`; do
echo "Signing" "$i"
gpg --batch --output "signatures/$i.sig" --detach-sig "$i"
done
cp sha256sum-$APP_VERSION.txt signatures/
cp ../configs/SIGNATURES_README signatures/
zip -r signatures-$APP_VERSION.zip signatures/

View File

@ -4,10 +4,9 @@ Verify the hashes by running:
sha256sum -c sha256sum-vX.Y.Z.txt
Verify signatures:
1. First, import the public key (Available on GitHub
1. First, import the public key (Available on GitHub
at https://github.com/ZcashFoundation/zecwallet/blob/master/public_key.asc)
gpg --import public_key.asc
2. Verify signature
2. Verify signature
gpg --verify <filename.sig> <downloaded-filename-to-verify>

View File

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

View File

@ -0,0 +1,51 @@
/**
* Base webpack config used across other specific configs
*/
import path from 'path';
import webpack from 'webpack';
import { dependencies as externals } from '../app/package.json';
export default {
externals: [...Object.keys(externals || {}), 'bufferutil', 'utf-8-validate'],
module: {
rules: [
{
test: /\.jsx?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
cacheDirectory: true
}
}
}
]
},
output: {
path: path.join(__dirname, '..', 'app'),
// https://github.com/webpack/webpack/issues/1114
libraryTarget: 'commonjs2'
},
/**
* Determine the array of extensions that should be used to resolve modules.
*/
resolve: {
extensions: ['.js', '.jsx', '.json'],
modules: [path.join(__dirname, '..', 'app'), 'node_modules'],
alias: {
ws: path.resolve(path.join(__dirname, '..', 'node_modules/ws/index.js'))
}
},
plugins: [
new webpack.EnvironmentPlugin({
NODE_ENV: 'production'
}),
new webpack.NamedModulesPlugin()
]
};

View File

@ -0,0 +1,4 @@
/* eslint import/no-unresolved: off, import/no-self-import: off */
require('@babel/register');
module.exports = require('./webpack.config.renderer.dev.babel').default;

View File

@ -0,0 +1,72 @@
/**
* Webpack config for production electron main process
*/
import path from 'path';
import webpack from 'webpack';
import merge from 'webpack-merge';
import TerserPlugin from 'terser-webpack-plugin';
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
import baseConfig from './webpack.config.base';
import CheckNodeEnv from '../internals/scripts/CheckNodeEnv';
CheckNodeEnv('production');
export default merge.smart(baseConfig, {
devtool: 'source-map',
mode: 'production',
target: 'electron-main',
entry: './app/main.dev',
output: {
path: path.join(__dirname, '..'),
filename: './app/main.prod.js'
},
optimization: {
minimizer: process.env.E2E_BUILD
? []
: [
new TerserPlugin({
parallel: true,
sourceMap: true,
cache: true
})
]
},
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: process.env.OPEN_ANALYZER === 'true' ? 'server' : 'disabled',
openAnalyzer: process.env.OPEN_ANALYZER === 'true'
}),
/**
* Create global constants which can be configured at compile time.
*
* Useful for allowing different behaviour between development builds and
* release builds
*
* NODE_ENV should be production so that modules do not perform certain
* development checks
*/
new webpack.EnvironmentPlugin({
NODE_ENV: 'production',
DEBUG_PROD: false,
START_MINIMIZED: false
})
],
/**
* Disables webpack processing of __dirname and __filename.
* If you run the bundle in node.js it falls back to these values of node.js.
* https://github.com/webpack/webpack/issues/2010
*/
node: {
__dirname: false,
__filename: false
}
});

View File

@ -0,0 +1,291 @@
/* eslint global-require: off, import/no-dynamic-require: off */
/**
* Build config for development electron renderer process that uses
* Hot-Module-Replacement
*
* https://webpack.js.org/concepts/hot-module-replacement/
*/
import path from 'path';
import fs from 'fs';
import webpack from 'webpack';
import chalk from 'chalk';
import merge from 'webpack-merge';
import { spawn, execSync } from 'child_process';
import baseConfig from './webpack.config.base';
import CheckNodeEnv from '../internals/scripts/CheckNodeEnv';
// When an ESLint server is running, we can't set the NODE_ENV so we'll check if it's
// at the dev webpack config is not accidentally run in a production environment
if (process.env.NODE_ENV === 'production') {
CheckNodeEnv('development');
}
const port = process.env.PORT || 1212;
const publicPath = `http://localhost:${port}/dist`;
const dll = path.join(__dirname, '..', 'dll');
const manifest = path.resolve(dll, 'renderer.json');
const requiredByDLLConfig = module.parent.filename.includes('webpack.config.renderer.dev.dll');
/**
* Warn if the DLL is not built
*/
if (!requiredByDLLConfig && !(fs.existsSync(dll) && fs.existsSync(manifest))) {
console.log(
chalk.black.bgYellow.bold('The DLL files are missing. Sit back while we build them for you with "yarn build-dll"')
);
execSync('yarn build-dll');
}
export default merge.smart(baseConfig, {
devtool: 'inline-source-map',
mode: 'development',
target: 'electron-renderer',
entry: [
...(process.env.PLAIN_HMR ? [] : ['react-hot-loader/patch']),
`webpack-dev-server/client?http://localhost:${port}/`,
'webpack/hot/only-dev-server',
require.resolve('../app/index')
],
output: {
publicPath: `http://localhost:${port}/dist/`,
filename: 'renderer.dev.js'
},
module: {
rules: [
{
test: /\.jsx?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
cacheDirectory: true
}
}
},
{
test: /\.global\.css$/,
use: [
{
loader: 'style-loader'
},
{
loader: 'css-loader',
options: {
sourceMap: true
}
}
]
},
{
test: /^((?!\.global).)*\.css$/,
use: [
{
loader: 'style-loader'
},
{
loader: 'css-loader',
options: {
modules: {
localIdentName: '[name]__[local]__[hash:base64:5]'
},
sourceMap: true,
importLoaders: 1
}
}
]
},
// SASS support - compile all .global.scss files and pipe it to style.css
{
test: /\.global\.(scss|sass)$/,
use: [
{
loader: 'style-loader'
},
{
loader: 'css-loader',
options: {
sourceMap: true
}
},
{
loader: 'sass-loader'
}
]
},
// SASS support - compile all other .scss files and pipe it to style.css
{
test: /^((?!\.global).)*\.(scss|sass)$/,
use: [
{
loader: 'style-loader'
},
{
loader: 'css-loader',
options: {
modules: {
localIdentName: '[name]__[local]__[hash:base64:5]'
},
sourceMap: true,
importLoaders: 1
}
},
{
loader: 'sass-loader'
}
]
},
// WOFF Font
{
test: /\.woff(\?v=\d+\.\d+\.\d+)?$/,
use: {
loader: 'url-loader',
options: {
limit: 10000,
mimetype: 'application/font-woff'
}
}
},
// WOFF2 Font
{
test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/,
use: {
loader: 'url-loader',
options: {
limit: 10000,
mimetype: 'application/font-woff'
}
}
},
// TTF Font
{
test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/,
use: {
loader: 'url-loader',
options: {
limit: 10000,
mimetype: 'application/octet-stream'
}
}
},
// EOT Font
{
test: /\.eot(\?v=\d+\.\d+\.\d+)?$/,
use: 'file-loader'
},
// SVG Font
{
test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
use: {
loader: 'url-loader',
options: {
limit: 10000,
mimetype: 'image/svg+xml'
}
}
},
// Common Image Formats
{
test: /\.(?:ico|gif|png|jpg|jpeg|webp)$/,
use: 'url-loader'
},
{
test: /\.node$/,
use: [
{
loader: path.resolve('./internals/scripts/NativeLoader.js'),
options: {
name: '[name]-[hash].[ext]'
}
}
]
}
]
},
resolve: {
alias: {
'react-dom': '@hot-loader/react-dom',
ws: path.resolve(path.join(__dirname, '..', 'node_modules/ws/index.js'))
}
},
plugins: [
requiredByDLLConfig
? null
: new webpack.DllReferencePlugin({
context: path.join(__dirname, '..', 'dll'),
manifest: require(manifest),
sourceType: 'var'
}),
new webpack.HotModuleReplacementPlugin({
multiStep: true
}),
new webpack.NoEmitOnErrorsPlugin(),
/**
* Create global constants which can be configured at compile time.
*
* Useful for allowing different behaviour between development builds and
* release builds
*
* NODE_ENV should be production so that modules do not perform certain
* development checks
*
* By default, use 'development' as NODE_ENV. This can be overridden with
* 'staging', for example, by changing the ENV variables in the npm scripts
*/
new webpack.EnvironmentPlugin({
NODE_ENV: 'development'
}),
new webpack.LoaderOptionsPlugin({
debug: true
})
],
node: {
__dirname: false,
__filename: false
},
devServer: {
port,
publicPath,
compress: true,
noInfo: true,
stats: 'errors-only',
inline: true,
lazy: false,
hot: true,
headers: { 'Access-Control-Allow-Origin': '*' },
contentBase: path.join(__dirname, 'dist'),
watchOptions: {
aggregateTimeout: 300,
ignored: /node_modules/,
poll: 100
},
historyApiFallback: {
verbose: true,
disableDotRule: false
},
before() {
if (process.env.START_HOT) {
console.log('Starting Main Process...');
spawn('npm', ['run', 'start-main-dev'], {
shell: true,
env: process.env,
stdio: 'inherit'
})
.on('close', code => process.exit(code))
.on('error', spawnError => console.error(spawnError));
}
}
}
});

View File

@ -0,0 +1,74 @@
/* eslint global-require: off, import/no-dynamic-require: off */
/**
* Builds the DLL for development electron renderer process
*/
import webpack from 'webpack';
import path from 'path';
import merge from 'webpack-merge';
import baseConfig from './webpack.config.base';
import { dependencies } from '../package.json';
import CheckNodeEnv from '../internals/scripts/CheckNodeEnv';
CheckNodeEnv('development');
const dist = path.join(__dirname, '..', 'dll');
export default merge.smart(baseConfig, {
context: path.join(__dirname, '..'),
devtool: 'eval',
mode: 'development',
target: 'electron-renderer',
externals: ['fsevents', 'crypto-browserify'],
/**
* Use `module` from `webpack.config.renderer.dev.js`
*/
module: require('./webpack.config.renderer.dev.babel').default.module,
entry: {
renderer: Object.keys(dependencies || {})
},
output: {
library: 'renderer',
path: dist,
filename: '[name].dev.dll.js',
libraryTarget: 'var'
},
plugins: [
new webpack.DllPlugin({
path: path.join(dist, '[name].json'),
name: '[name]'
}),
/**
* Create global constants which can be configured at compile time.
*
* Useful for allowing different behaviour between development builds and
* release builds
*
* NODE_ENV should be production so that modules do not perform certain
* development checks
*/
new webpack.EnvironmentPlugin({
NODE_ENV: 'development'
}),
new webpack.LoaderOptionsPlugin({
debug: true,
options: {
context: path.join(__dirname, '..', 'app'),
output: {
path: path.join(__dirname, '..', 'dll')
}
}
})
]
});

View File

@ -0,0 +1,228 @@
/**
* Build config for electron renderer process
*/
import path from 'path';
import webpack from 'webpack';
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import OptimizeCSSAssetsPlugin from 'optimize-css-assets-webpack-plugin';
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
import merge from 'webpack-merge';
import TerserPlugin from 'terser-webpack-plugin';
import baseConfig from './webpack.config.base';
import CheckNodeEnv from '../internals/scripts/CheckNodeEnv';
CheckNodeEnv('production');
export default merge.smart(baseConfig, {
devtool: 'source-map',
mode: 'production',
target: 'electron-renderer',
entry: path.join(__dirname, '..', 'app/index'),
output: {
path: path.join(__dirname, '..', 'app/dist'),
publicPath: './dist/',
filename: 'renderer.prod.js'
},
module: {
rules: [
// Extract all .global.css to style.css as is
{
test: /\.global\.css$/,
use: [
{
loader: MiniCssExtractPlugin.loader,
options: {
publicPath: './'
}
},
{
loader: 'css-loader',
options: {
sourceMap: true
}
}
]
},
// Pipe other styles through css modules and append to style.css
{
test: /^((?!\.global).)*\.css$/,
use: [
{
loader: MiniCssExtractPlugin.loader
},
{
loader: 'css-loader',
options: {
modules: {
localIdentName: '[name]__[local]__[hash:base64:5]'
},
sourceMap: true
}
}
]
},
// Add SASS support - compile all .global.scss files and pipe it to style.css
{
test: /\.global\.(scss|sass)$/,
use: [
{
loader: MiniCssExtractPlugin.loader
},
{
loader: 'css-loader',
options: {
sourceMap: true,
importLoaders: 1
}
},
{
loader: 'sass-loader',
options: {
sourceMap: true
}
}
]
},
// Add SASS support - compile all other .scss files and pipe it to style.css
{
test: /^((?!\.global).)*\.(scss|sass)$/,
use: [
{
loader: MiniCssExtractPlugin.loader
},
{
loader: 'css-loader',
options: {
modules: {
localIdentName: '[name]__[local]__[hash:base64:5]'
},
importLoaders: 1,
sourceMap: true
}
},
{
loader: 'sass-loader',
options: {
sourceMap: true
}
}
]
},
// WOFF Font
{
test: /\.woff(\?v=\d+\.\d+\.\d+)?$/,
use: {
loader: 'url-loader',
options: {
limit: 10000,
mimetype: 'application/font-woff'
}
}
},
// WOFF2 Font
{
test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/,
use: {
loader: 'url-loader',
options: {
limit: 10000,
mimetype: 'application/font-woff'
}
}
},
// TTF Font
{
test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/,
use: {
loader: 'url-loader',
options: {
limit: 10000,
mimetype: 'application/octet-stream'
}
}
},
// EOT Font
{
test: /\.eot(\?v=\d+\.\d+\.\d+)?$/,
use: 'file-loader'
},
// SVG Font
{
test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
use: {
loader: 'url-loader',
options: {
limit: 10000,
mimetype: 'image/svg+xml'
}
}
},
// Common Image Formats
{
test: /\.(?:ico|gif|png|jpg|jpeg|webp)$/,
use: 'url-loader'
},
{
test: /\.node$/,
use: [
{
loader: path.resolve('./internals/scripts/NativeLoader.js'),
options: {
name: '[name]-[hash].[ext]'
}
}
]
}
]
},
optimization: {
minimizer: process.env.E2E_BUILD
? []
: [
new TerserPlugin({
parallel: true,
sourceMap: true,
cache: true
}),
new OptimizeCSSAssetsPlugin({
cssProcessorOptions: {
map: {
inline: false,
annotation: true
}
}
})
]
},
plugins: [
/**
* Create global constants which can be configured at compile time.
*
* Useful for allowing different behaviour between development builds and
* release builds
*
* NODE_ENV should be production so that modules do not perform certain
* development checks
*/
new webpack.EnvironmentPlugin({
NODE_ENV: 'production'
}),
new MiniCssExtractPlugin({
filename: 'style.css'
}),
new BundleAnalyzerPlugin({
analyzerMode: process.env.OPEN_ANALYZER === 'true' ? 'server' : 'disabled',
openAnalyzer: process.env.OPEN_ANALYZER === 'true'
})
]
});

View File

@ -1,6 +0,0 @@
<html>
<head></head>
<body>
Hello World
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

3
flow-typed/module_vx.x.x.js vendored Normal file
View File

@ -0,0 +1,3 @@
declare module 'module' {
declare module.exports: any;
}

View File

@ -0,0 +1,3 @@
// @flow
declare export default { [key: string]: string }

View File

@ -0,0 +1,2 @@
// @flow
declare export default string

View File

@ -0,0 +1,5 @@
const path = require('path');
require('@babel/register')({
cwd: path.join(__dirname, '..', '..')
});

View File

@ -0,0 +1,35 @@
// @flow
// Check if the renderer and main bundles are built
import path from 'path';
import chalk from 'chalk';
import fs from 'fs';
function CheckBuildsExist() {
const mainPath = path.join(__dirname, '..', '..', 'app', 'main.prod.js');
const rendererPath = path.join(
__dirname,
'..',
'..',
'app',
'dist',
'renderer.prod.js'
);
if (!fs.existsSync(mainPath)) {
throw new Error(
chalk.whiteBright.bgRed.bold(
'The main process is not built yet. Build it by running "yarn build-main"'
)
);
}
if (!fs.existsSync(rendererPath)) {
throw new Error(
chalk.whiteBright.bgRed.bold(
'The renderer process is not built yet. Build it by running "yarn build-renderer"'
)
);
}
}
CheckBuildsExist();

View File

@ -0,0 +1,51 @@
// @flow
import fs from 'fs';
import chalk from 'chalk';
import { execSync } from 'child_process';
import { dependencies } from '../../package.json';
(() => {
if (!dependencies) return;
const dependenciesKeys = Object.keys(dependencies);
const nativeDeps = fs
.readdirSync('node_modules')
.filter(folder => fs.existsSync(`node_modules/${folder}/binding.gyp`));
try {
// Find the reason for why the dependency is installed. If it is installed
// because of a devDependency then that is okay. Warn when it is installed
// because of a dependency
const { dependencies: dependenciesObject } = JSON.parse(
execSync(`npm ls ${nativeDeps.join(' ')} --json`).toString()
);
const rootDependencies = Object.keys(dependenciesObject);
const filteredRootDependencies = rootDependencies.filter(rootDependency =>
dependenciesKeys.includes(rootDependency)
);
if (filteredRootDependencies.length > 0) {
const plural = filteredRootDependencies.length > 1;
console.log(`
${chalk.whiteBright.bgYellow.bold(
'Webpack does not work with native dependencies.'
)}
${chalk.bold(filteredRootDependencies.join(', '))} ${
plural ? 'are native dependencies' : 'is a native dependency'
} and should be installed inside of the "./app" folder.
First uninstall the packages from "./package.json":
${chalk.whiteBright.bgGreen.bold('yarn remove your-package')}
${chalk.bold(
'Then, instead of installing the package to the root "./package.json":'
)}
${chalk.whiteBright.bgRed.bold('yarn add your-package')}
${chalk.bold('Install the package to "./app/package.json"')}
${chalk.whiteBright.bgGreen.bold('cd ./app && yarn add your-package')}
Read more about native dependencies at:
${chalk.bold(
'https://github.com/electron-react-boilerplate/electron-react-boilerplate/wiki/Module-Structure----Two-package.json-Structure'
)}
`);
process.exit(1);
}
} catch (e) {
console.log('Native dependencies could not be checked');
}
})();

View File

@ -0,0 +1,17 @@
// @flow
import chalk from 'chalk';
export default function CheckNodeEnv(expectedEnv: string) {
if (!expectedEnv) {
throw new Error('"expectedEnv" not set');
}
if (process.env.NODE_ENV !== expectedEnv) {
console.log(
chalk.whiteBright.bgRed.bold(
`"process.env.NODE_ENV" must be "${expectedEnv}" to use this webpack config`
)
);
process.exit(2);
}
}

View File

@ -0,0 +1,19 @@
// @flow
import chalk from 'chalk';
import detectPort from 'detect-port';
(function CheckPortInUse() {
const port: string = process.env.PORT || '1212';
detectPort(port, (err: ?Error, availablePort: number) => {
if (port !== String(availablePort)) {
throw new Error(
chalk.whiteBright.bgRed.bold(
`Port "${port}" on "localhost" is already in use. Please use another port. ex: PORT=4343 yarn dev`
)
);
} else {
process.exit(0);
}
});
})();

View File

@ -0,0 +1,7 @@
// @flow
if (!/yarn\.js$/.test(process.env.npm_execpath || '')) {
console.warn(
"\u001b[33mYou don't seem to be using yarn. This could produce unexpected results.\u001b[39m"
);
}

View File

@ -0,0 +1,31 @@
// @flow
import path from 'path';
import { execSync } from 'child_process';
import fs from 'fs';
import { dependencies } from '../../app/package.json';
(() => {
const nodeModulesPath = path.join(
__dirname,
'..',
'..',
'app',
'node_modules'
);
if (
Object.keys(dependencies || {}).length > 0 &&
fs.existsSync(nodeModulesPath)
) {
const electronRebuildCmd =
'../node_modules/.bin/electron-rebuild --parallel --force --types prod,dev,optional --module-dir .';
const cmd =
process.platform === 'win32'
? electronRebuildCmd.replace(/\//g, '\\')
: electronRebuildCmd;
execSync(cmd, {
cwd: path.join(__dirname, '..', '..', 'app'),
stdio: 'inherit'
});
}
})();

Some files were not shown because too many files have changed in this diff Show More