From d670b5c5679e05d4845c5ace72025808092cf390 Mon Sep 17 00:00:00 2001 From: ArqTras <33489188+ArqTras@users.noreply.github.com> Date: Tue, 10 Dec 2019 10:28:35 +0100 Subject: [PATCH 1/2] Pool Solo Pool Co-Authored-By: mosu-forge Co-Authored-By: Mikunj Co-Authored-By: muscleman Co-Authored-By: Michal vel m@lbit Co-Authored-By: kuczys Co-Authored-By: ArqTras --- package-lock.json | 544 ++++++- package.json | 12 +- quasar.conf.js | 43 +- src-electron/build/ryo-dmg.tiff | Bin 0 -> 91922 bytes src-electron/icons/icon-32x32.png | Bin 3517 -> 0 bytes src-electron/icons/icon-512x512.png | Bin 68907 -> 0 bytes src-electron/main-process/electron-main.js | 203 +-- src-electron/main-process/modules/backend.js | 958 ++++++------ src-electron/main-process/modules/daemon.js | 398 +++-- src-electron/main-process/modules/market.js | 2 +- src-electron/main-process/modules/pool.js | 718 +++++++++ .../main-process/modules/pool/block.js | 55 + .../main-process/modules/pool/database.js | 350 +++++ .../main-process/modules/pool/miner.js | 187 +++ .../main-process/modules/pool/utils.js | 70 + .../main-process/modules/status-codes.js | 3 - .../main-process/modules/wallet-rpc.js | 1351 +++++++---------- src/components/footer.vue | 84 +- src/components/format_bitcoin.vue | 3 +- src/components/format_ryo.vue | 9 +- src/components/identicon.vue | 30 +- src/components/mainmenu.vue | 17 +- src/components/pool.vue | 102 +- src/components/receive_item.vue | 125 -- src/components/service_node_registration.vue | 121 -- src/components/service_node_staking.vue | 275 ---- src/components/service_node_unlock.vue | 212 --- src/components/settings.vue | 223 ++- src/components/settings_general.vue | 324 ++-- src/components/wallet_details.vue | 307 ---- src/components/wallet_settings.vue | 520 ------- src/gateway/gateway.js | 316 ++-- src/pages/wallet-select/import-old-gui.vue | 139 -- src/pages/wallet-select/restore.vue | 3 +- src/pages/wallet/send.vue | 27 +- src/pages/wallet/service-node.vue | 36 - src/plugins/vuelidate.js | 2 +- src/router/routes.js | 50 +- src/statics/icons/16x16.png | Bin 1030 -> 0 bytes src/statics/icons/24x24.png | Bin 1948 -> 0 bytes src/statics/icons/32x32.png | Bin 2965 -> 0 bytes src/statics/icons/48x48.png | Bin 5521 -> 0 bytes src/statics/icons/64x64.png | Bin 8343 -> 0 bytes src/statics/icons/96x96.png | Bin 15331 -> 0 bytes src/statics/icons/icon-128x128.png | Bin 19525 -> 0 bytes src/statics/icons/icon-192x192.png | Bin 33336 -> 0 bytes src/statics/icons/icon-256x256.png | Bin 49318 -> 0 bytes src/statics/icons/icon-384x384.png | Bin 84460 -> 0 bytes src/statics/icons/icon-512x512.png | Bin 122910 -> 0 bytes src/statics/icons/linux-512x512.png | Bin 122910 -> 0 bytes src/statics/qr-code-grey.svg | 3 - src/statics/qr-code.svg | 3 - src/statics/ryo-wallet.svg | 1274 ++-------------- src/store/gateway/index.js | 10 +- src/store/gateway/mutations.js | 11 +- src/store/gateway/state.js | 66 +- src/validators/common.js | 45 +- 57 files changed, 4133 insertions(+), 5098 deletions(-) create mode 100644 src-electron/build/ryo-dmg.tiff delete mode 100644 src-electron/icons/icon-32x32.png delete mode 100644 src-electron/icons/icon-512x512.png create mode 100644 src-electron/main-process/modules/pool.js create mode 100644 src-electron/main-process/modules/pool/block.js create mode 100644 src-electron/main-process/modules/pool/database.js create mode 100644 src-electron/main-process/modules/pool/miner.js create mode 100644 src-electron/main-process/modules/pool/utils.js delete mode 100644 src-electron/main-process/modules/status-codes.js delete mode 100644 src/components/receive_item.vue delete mode 100644 src/components/service_node_registration.vue delete mode 100644 src/components/service_node_staking.vue delete mode 100644 src/components/service_node_unlock.vue delete mode 100644 src/components/wallet_details.vue delete mode 100644 src/components/wallet_settings.vue delete mode 100644 src/pages/wallet-select/import-old-gui.vue delete mode 100644 src/pages/wallet/service-node.vue delete mode 100644 src/statics/icons/16x16.png delete mode 100644 src/statics/icons/24x24.png delete mode 100644 src/statics/icons/32x32.png delete mode 100644 src/statics/icons/48x48.png delete mode 100644 src/statics/icons/64x64.png delete mode 100644 src/statics/icons/96x96.png delete mode 100644 src/statics/icons/icon-128x128.png delete mode 100644 src/statics/icons/icon-192x192.png delete mode 100644 src/statics/icons/icon-256x256.png delete mode 100644 src/statics/icons/icon-384x384.png delete mode 100644 src/statics/icons/icon-512x512.png delete mode 100644 src/statics/icons/linux-512x512.png delete mode 100644 src/statics/qr-code-grey.svg delete mode 100644 src/statics/qr-code.svg diff --git a/package-lock.json b/package-lock.json index f319dff..d6effca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "arqma-electron-wallet", - "version": "2.0.6", + "version": "2.0.7", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -3263,6 +3263,36 @@ "tweetnacl": "^0.14.3" } }, + "better-sqlite3": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-5.4.3.tgz", + "integrity": "sha512-fPp+8f363qQIhuhLyjI4bu657J/FfMtgiiHKfaTsj3RWDkHlWC1yT7c6kHZDnBxzQVoAINuzg553qKmZ4F1rEw==", + "requires": { + "integer": "^2.1.0", + "tar": "^4.4.10" + }, + "dependencies": { + "tar": { + "version": "4.4.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz", + "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==", + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.8.6", + "minizlib": "^1.2.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.3" + } + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + } + } + }, "bfj": { "version": "6.1.2", "resolved": "https://registry.npmjs.org/bfj/-/bfj-6.1.2.tgz", @@ -3283,11 +3313,15 @@ } } }, + "big-integer": { + "version": "1.6.48", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.48.tgz", + "integrity": "sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w==" + }, "big.js": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz", - "integrity": "sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==", - "dev": true + "integrity": "sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==" }, "binary-extensions": { "version": "1.13.1", @@ -4049,8 +4083,7 @@ "chownr": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.2.tgz", - "integrity": "sha512-GkfeAQh+QNy3wquu9oIZr6SS5x7wGdSgNQvD10X3r+AZr1Oys22HW8kAmDMvNg2+Dm0TeGaEuO8gFwdBXxwO8A==", - "dev": true + "integrity": "sha512-GkfeAQh+QNy3wquu9oIZr6SS5x7wGdSgNQvD10X3r+AZr1Oys22HW8kAmDMvNg2+Dm0TeGaEuO8gFwdBXxwO8A==" }, "chrome-trace-event": { "version": "1.0.2", @@ -4138,6 +4171,12 @@ "restore-cursor": "^2.0.0" } }, + "cli-spinners": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.2.0.tgz", + "integrity": "sha512-tgU3fKwzYjiLEQgPMD9Jt+JjHVL9kW93FiIMX/l7rivvOD4/LL0Mf7gda3+4U2KJBloybwgj5KEoQgGRioMiKQ==", + "dev": true + }, "cli-width": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", @@ -4172,6 +4211,12 @@ } } }, + "clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", + "dev": true + }, "clone-deep": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-2.0.2.tgz", @@ -5138,6 +5183,11 @@ "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=", "dev": true }, + "dateformat": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz", + "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==" + }, "de-indent": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", @@ -5237,6 +5287,15 @@ } } }, + "defaults": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", + "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", + "dev": true, + "requires": { + "clone": "^1.0.2" + } + }, "defer-to-connect": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.0.tgz", @@ -5369,6 +5428,12 @@ "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=", "dev": true }, + "detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", + "dev": true + }, "detect-node": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.4.tgz", @@ -6138,6 +6203,271 @@ } } }, + "electron-rebuild": { + "version": "1.8.8", + "resolved": "https://registry.npmjs.org/electron-rebuild/-/electron-rebuild-1.8.8.tgz", + "integrity": "sha512-9a/VGbVpTJcuBaZa8yMcegqJ5flGPYDo363AxXDMxY4ZHPtFMLedGzQW9+720SIS1cvjX8B0zC+vMHO75ncOiA==", + "dev": true, + "requires": { + "colors": "^1.3.3", + "debug": "^4.1.1", + "detect-libc": "^1.0.3", + "fs-extra": "^7.0.1", + "node-abi": "^2.11.0", + "node-gyp": "^6.0.1", + "ora": "^3.4.0", + "spawn-rx": "^3.0.0", + "yargs": "^13.2.4" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dev": true, + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + } + }, + "colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "dev": true + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "env-paths": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.0.tgz", + "integrity": "sha512-6u0VYSCo/OW6IoD5WCLLy9JUGARbamfSavcNXry/eu8aHVFei6CD3Sw+VGX5alea1i9pgPHW0mbu6Xj0uBh7gA==", + "dev": true + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "graceful-fs": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", + "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==", + "dev": true + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node-gyp": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-6.0.1.tgz", + "integrity": "sha512-udHG4hGe3Ji97AYJbJhaRwuSOuQO7KHnE4ZPH3Sox3tjRZ+bkBsDvfZ7eYA1qwD8eLWr//193x806ss3HFTPRw==", + "dev": true, + "requires": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.2", + "mkdirp": "^0.5.1", + "nopt": "^4.0.1", + "npmlog": "^4.1.2", + "request": "^2.88.0", + "rimraf": "^2.6.3", + "semver": "^5.7.1", + "tar": "^4.4.12", + "which": "^1.3.1" + } + }, + "nopt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", + "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", + "dev": true, + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "p-limit": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.1.tgz", + "integrity": "sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "tar": { + "version": "4.4.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz", + "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==", + "dev": true, + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.8.6", + "minizlib": "^1.2.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.3" + } + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + } + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "yargs": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.0.tgz", + "integrity": "sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA==", + "dev": true, + "requires": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.1" + } + }, + "yargs-parser": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz", + "integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, "electron-to-chromium": { "version": "1.3.188", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.188.tgz", @@ -6253,8 +6583,7 @@ "emojis-list": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", - "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=", - "dev": true + "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=" }, "encodeurl": { "version": "1.0.2", @@ -6885,6 +7214,22 @@ } } }, + "exports-loader": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/exports-loader/-/exports-loader-0.7.0.tgz", + "integrity": "sha512-RKwCrO4A6IiKm0pG3c9V46JxIHcDplwwGJn6+JJ1RcVnh/WSGJa0xkmk5cRVtgOPzCAtTMGj2F7nluh9L0vpSA==", + "requires": { + "loader-utils": "^1.1.0", + "source-map": "0.5.0" + }, + "dependencies": { + "source-map": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.0.tgz", + "integrity": "sha1-D+llA6yGpa213mP05BKuSHLNvoY=" + } + } + }, "express": { "version": "4.16.4", "resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz", @@ -7412,6 +7757,14 @@ "universalify": "^0.1.0" } }, + "fs-minipass": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", + "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", + "requires": { + "minipass": "^2.6.0" + } + }, "fs-write-stream-atomic": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", @@ -7450,7 +7803,8 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -7471,12 +7825,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -7491,17 +7847,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -7618,7 +7977,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -7630,6 +7990,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -7644,6 +8005,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -7651,12 +8013,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -7675,6 +8039,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -7755,7 +8120,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -7767,6 +8133,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -7852,7 +8219,8 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -7888,6 +8256,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -7907,6 +8276,7 @@ "version": "3.0.1", "bundled": true, "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -7950,12 +8320,14 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true } } }, @@ -8966,6 +9338,11 @@ } } }, + "integer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/integer/-/integer-2.1.0.tgz", + "integrity": "sha512-vBtiSgrEiNocWvvZX1RVfeOKa2mCHLZQ2p9nkQkQZ/BvEiY+6CcUz0eyjvIiewjJoeNidzg2I+tpPJvpyspL1w==" + }, "internal-ip": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-3.0.1.tgz", @@ -9532,8 +9909,7 @@ "json5": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", - "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", - "dev": true + "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=" }, "jsonfile": { "version": "4.0.0", @@ -9668,7 +10044,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.1.0.tgz", "integrity": "sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0=", - "dev": true, "requires": { "big.js": "^3.1.3", "emojis-list": "^2.0.0", @@ -9811,6 +10186,15 @@ "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=", "dev": true }, + "log-symbols": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", + "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", + "dev": true, + "requires": { + "chalk": "^2.0.1" + } + }, "log-update": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/log-update/-/log-update-2.3.0.tgz", @@ -10201,6 +10585,30 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" }, + "minipass": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", + "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + }, + "dependencies": { + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + } + } + }, + "minizlib": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", + "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", + "requires": { + "minipass": "^2.9.0" + } + }, "mississippi": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", @@ -10376,6 +10784,15 @@ "lower-case": "^1.1.1" } }, + "node-abi": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.12.0.tgz", + "integrity": "sha512-VhPBXCIcvmo/5K8HPmnWJyyhvgKxnHTUMXR/XwGHV68+wrgkzST4UmQrY/XszSWA5dtnXpNp528zkcyJ/pzVcw==", + "dev": true, + "requires": { + "semver": "^5.4.1" + } + }, "node-forge": { "version": "0.7.5", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.5.tgz", @@ -10987,6 +11404,48 @@ "wordwrap": "~1.0.0" } }, + "ora": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-3.4.0.tgz", + "integrity": "sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "cli-cursor": "^2.1.0", + "cli-spinners": "^2.0.0", + "log-symbols": "^2.2.0", + "strip-ansi": "^5.2.0", + "wcwidth": "^1.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, "original": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/original/-/original-1.0.2.tgz", @@ -13200,6 +13659,11 @@ "inherits": "^2.0.1" } }, + "rotating-file-stream": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/rotating-file-stream/-/rotating-file-stream-1.4.6.tgz", + "integrity": "sha512-QS7vGxBK6sGc1mCqlmAuwV4J0fmmVCKaUgMvKbkTueZr4jdkXN3bSpTEOQxtdtAVEzi1aUqdHzwIQ0ejNn+CQg==" + }, "route-cache": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/route-cache/-/route-cache-0.4.4.tgz", @@ -13281,6 +13745,13 @@ "tslib": "^1.9.0" } }, + "ryo-core-js": { + "version": "git://github.com/arqtras/ryo-core-js.git#845f46e481f195eff8feac72d7d110ef22db9d21", + "from": "git://github.com/arqtras/ryo-core-js.git", + "requires": { + "big-integer": "^1.6.41" + } + }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -13872,6 +14343,28 @@ "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", "dev": true }, + "spawn-rx": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spawn-rx/-/spawn-rx-3.0.0.tgz", + "integrity": "sha512-dw4Ryg/KMNfkKa5ezAR5aZe9wNwPdKlnHEXtHOjVnyEDSPQyOpIPPRtcIiu7127SmtHhaCjw21yC43HliW0iIg==", + "dev": true, + "requires": { + "debug": "^2.5.1", + "lodash.assign": "^4.2.0", + "rxjs": "^6.3.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } + } + }, "spdx-correct": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.0.tgz", @@ -15668,6 +16161,15 @@ "minimalistic-assert": "^1.0.0" } }, + "wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", + "dev": true, + "requires": { + "defaults": "^1.0.3" + } + }, "webpack": { "version": "4.39.2", "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.39.2.tgz", diff --git a/package.json b/package.json index 3860938..f9169f1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "arqma-electron-wallet", - "version": "2.0.6", + "version": "2.0.7", "daemonVersion": "6.0.0", "description": "Modern GUI interface for Arqma Currency", "productName": "Arqma Electron Wallet", @@ -18,19 +18,24 @@ "dev": "quasar dev -m electron -t mat", "build": "quasar build -m electron -t mat", "lint": "eslint --ext .js,.vue src", + "rebuild": "electron-rebuild -f -w better-sqlite3", "lint-fix": "eslint --fix .", "test": "echo \"No test specified\" && exit 0" }, "dependencies": { "acorn": "^6.2.1", "axios": "^0.19.0", + "better-sqlite3": "^5.4.0", + "big-integer": "^1.6.41", "buffer": "^5.2.1", "bufferutil": "*", "chart.js": "^2.8.0", + "dateformat": "^3.0.3", "electron-is-dev": "^1.0.1", "electron-progressbar": "^1.2.0", "electron-updater": "^4.0.6", "electron-window-state": "^5.0.3", + "exports-loader": "^0.7.0", "flag-icon-css": "^3.3.0", "fs-extra": "^7.0.1", "graceful-fs": "^4.2.1", @@ -40,9 +45,11 @@ "qrcode.vue": "^1.6.1", "request": "^2.88.0", "request-promise": "^4.2.4", + "rotating-file-stream": "^1.4.1", + "ryo-core-js": "git://github.com/arqtras/ryo-core-js.git", + "vue-chartjs": "^3.4.2", "utf-8-validate": "^5.0.2", "validation": "0.0.1", - "vue-chartjs": "^3.4.2", "vue-i18n": "^8.9.0", "vue-timeago": "^5.1.2", "vuelidate": "^0.7.4", @@ -56,6 +63,7 @@ "electron-builder": "^21.2.0", "electron-debug": "^2.1.0", "electron-devtools-installer": "^2.2.4", + "electron-rebuild": "^1.8.4", "electron-notarize": "^0.1.1", "electron-packager": "^13.1.1", "eslint": "^5.15.3", diff --git a/quasar.conf.js b/quasar.conf.js index f090ac9..b852843 100644 --- a/quasar.conf.js +++ b/quasar.conf.js @@ -29,7 +29,20 @@ module.exports = function (ctx) { // gzip: true, // analyze: true, // extractCSS: false, - extendWebpack (cfg) { + sourceMap: true, + extendWebpack(cfg) { + cfg.module.rules.push({ + test: /RyoCoreCpp\.js$/, + loader: "exports-loader" + }) + cfg.module.rules.push({ + test: /RyoCoreCpp\.wasm$/, + type: "javascript/auto", + loader: "file-loader", + options: { + name: "[name]-[hash].[ext]", + } + }) /* cfg.module.rules.push({ enforce: "pre", @@ -92,7 +105,12 @@ module.exports = function (ctx) { "QInfiniteScroll", "QDatetime", "QContextMenu", - "QScrollArea" + "QScrollArea", + "QTable", + "QTh", + "QTr", + "QTd", + "QTableColumns" ], directives: [ "Ripple", @@ -154,10 +172,21 @@ module.exports = function (ctx) { // id: "org.cordova.quasar.app" }, electron: { - bundler: "builder", // or "packager" - extendWebpack (cfg) { - // do something with Electron process Webpack cfg - }, + bundler: "builder", // or "packager" + extendWebpack(cfg) { + cfg.module.rules.push({ + test: /RyoCoreCpp\.js$/, + loader: "exports-loader" + }) + cfg.module.rules.push({ + test: /RyoCoreCpp\.wasm$/, + type: "javascript/auto", + loader: "file-loader", + options: { + name: "[name].[ext]", + } + }) + }, packager: { // https://github.com/electron-userland/electron-packager/blob/master/docs/api.md#options @@ -179,7 +208,7 @@ module.exports = function (ctx) { appId: "com.arqma.electron-wallet", productName: "Arqma Electron Wallet", - copyright: "Copyright © 2018-2019 Arqma Project, 2018 Ryo/Loki Currency Project", + copyright: "Copyright © 2018-2020 Arqma Project, 2018-2020 Ryo/Loki Currency Project", afterSign: "build/notarize.js", // directories: { diff --git a/src-electron/build/ryo-dmg.tiff b/src-electron/build/ryo-dmg.tiff new file mode 100644 index 0000000000000000000000000000000000000000..0d1686430ee477c00d9dbe79746f651671a36a2b GIT binary patch literal 91922 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

3cBNJ>?%)%no{9WXT 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%0D#zn0ng}_%evpS{D_Bin2Dj7VI6IX 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

0k49M$ih!}Dl_fk#vu%R`sw}M{ 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 A)c^nh literal 0 HcmV?d00001 diff --git a/src-electron/icons/icon-32x32.png b/src-electron/icons/icon-32x32.png deleted file mode 100644 index 7d28c098d3367e38666798d80c53fb1f3dc6dbc6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3517 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 diff --git a/src-electron/icons/icon-512x512.png b/src-electron/icons/icon-512x512.png deleted file mode 100644 index 26e6036154aebc5247ff8198d9f483cdb34ab4a0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 68907 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 X(n-)JArXrt00000NkvXXu0mjfd*P9m diff --git a/src-electron/main-process/electron-main.js b/src-electron/main-process/electron-main.js index 47aff15..8d5f7f1 100644 --- a/src-electron/main-process/electron-main.js +++ b/src-electron/main-process/electron-main.js @@ -1,53 +1,36 @@ -import { app, ipcMain, BrowserWindow, Menu, dialog } from "electron" -import { version, productName } from "../../package.json" +import { app, ipcMain, BrowserWindow, Menu, Tray, dialog } from "electron" import { Backend } from "./modules/backend" import { checkForUpdate } from "./auto-updater" import menuTemplate from "./menu" import isDev from "electron-is-dev" -const portscanner = require("portscanner") const windowStateKeeper = require("electron-window-state") +const path = require("path"); /** * Set `__statics` path to static files in production; * The reason we are setting it here is that the path needs to be evaluated at runtime */ if (process.env.PROD) { - global.__statics = require("path").join(__dirname, "statics").replace(/\\/g, "\\\\") - global.__arqma_bin = require("path").join(__dirname, "..", "bin").replace(/\\/g, "\\\\") + global.__statics = path.join(__dirname, "statics").replace(/\\/g, "\\\\") + global.__arqma_bin = path.join(__dirname, "..", "bin").replace(/\\/g, "\\\\") } else { - global.__arqma_bin = require("path").join(process.cwd(), "bin").replace(/\\/g, "\\\\") + global.__arqma_bin = path.join(process.cwd(), "bin").replace(/\\/g, "\\\\") } -let mainWindow, backend +let mainWindow, backend, tray let showConfirmClose = true let forceQuit = false +let updateTrayInterval = null let installUpdate = false -// eslint-disable-next-line no-unused-vars -const title = `${productName} v${version}` - -const selectionMenu = Menu.buildFromTemplate([ - { role: "copy" }, - { type: "separator" }, - { role: "selectall" } -]) - -const inputMenu = Menu.buildFromTemplate([ - { role: "cut" }, - { role: "copy" }, - { role: "paste" }, - { type: "separator" }, - { role: "selectall" } -]) - -function createWindow () { +function createWindow() { /** * Initial window options */ let mainWindowState = windowStateKeeper({ - defaultWidth: 900, - defaultHeight: 700 + defaultWidth: 800, + defaultHeight: 650 }) mainWindow = new BrowserWindow({ @@ -57,14 +40,11 @@ function createWindow () { height: mainWindowState.height, minWidth: 640, minHeight: 480, - icon: require("path").join(__statics, "icon_512x512.png"), - title: `${productName} v${version}` + icon: path.join(__statics, "icon_64x64.png") }) mainWindow.on("close", (e) => { - // Don't ask for confirmation if we're installing an update if (installUpdate) { return } - if (process.platform === "darwin") { if (forceQuit) { forceQuit = false @@ -92,83 +72,124 @@ function createWindow () { ipcMain.on("confirmClose", (e, restart) => { showConfirmClose = false - // In dev mode, this will launch a blank white screen - if (restart && !isDev) app.relaunch() + if(restart && !isDev) { + app.relaunch() + } - const promise = backend ? backend.quit() : Promise.resolve() - promise.then(() => { - backend = null + if (backend) { + if (process.platform !== "darwin") { + clearInterval(updateTrayInterval) + tray.setToolTip("Closing...") + } + backend.quit().then(() => { + backend = null + app.quit() + }) + } else { app.quit() - }) + } }) - mainWindow.webContents.on("did-finish-load", () => { - // Set the title - mainWindow.setTitle(`${productName} v${version}`) - - require("crypto").randomBytes(64, (err, buffer) => { - // if err, then we may have to use insecure token generation perhaps - if (err) throw err - - let config = { - port: 12313, - token: buffer.toString("hex") - } - - portscanner.checkPortStatus(config.port, "127.0.0.1", (e, status) => { - // if (error) { - // console.error(error) - // } + mainWindow.on("minimize", (e) => { + if (!backend || !backend.config_data) { + e.defaultPrevented = false + return + } + let minimize_to_tray = backend.config_data.preference.minimize_to_tray + if (minimize_to_tray === null) { + mainWindow.webContents.send("confirmMinimizeTray") + e.preventDefault() + } else if (minimize_to_tray === true) { + e.preventDefault() + mainWindow.hide() + } else { + e.defaultPrevented = false + } + }) - if (status === "closed") { - backend = new Backend(mainWindow) - backend.init(config) - mainWindow.webContents.send("initialize", config) - } else { - dialog.showMessageBox(mainWindow, { - title: "Startup error", - message: `Arqma Wallet is already open, or port ${config.port} is in use`, - type: "error", - buttons: ["ok"] - }, () => { - showConfirmClose = false - app.quit() - }) - } - }) + ipcMain.on("autostartSettings", (e, openAtLogin) => { + app.setLoginItemSettings({ + openAtLogin }) }) - mainWindow.webContents.on("context-menu", (e, props) => { - const { selectionText, isEditable } = props - if (isEditable) { - inputMenu.popup(mainWindow) - } else if (selectionText && selectionText.trim() !== "") { - selectionMenu.popup(mainWindow) + ipcMain.on("confirmMinimizeTray", (e, minimize_to_tray) => { + mainWindow.setMinimizable(true) + backend.config_data.preference.minimize_to_tray = minimize_to_tray + if (minimize_to_tray) { + mainWindow.hide() + } else { + mainWindow.minimize() } }) + mainWindow.webContents.on("did-finish-load", () => { + backend = new Backend(mainWindow) + backend.init() + }) + mainWindow.loadURL(process.env.APP_URL) mainWindowState.manage(mainWindow) } app.on("ready", () => { - checkForUpdate(autoUpdater => { - if (mainWindow) { - mainWindow.webContents.send("showQuitScreen") - } - - const promise = backend ? backend.quit() : Promise.resolve() - promise.then(() => { - installUpdate = true - backend = null - autoUpdater.quitAndInstall() - }) - }) + checkForUpdate(autoUpdater => { + if (mainWindow) { + mainWindow.webContents.send("showQuitScreen") + } + + const promise = backend ? backend.quit() : Promise.resolve() + promise.then(() => { + installUpdate = true + backend = null + autoUpdater.quitAndInstall() + }) + }) if (process.platform === "darwin") { const menu = Menu.buildFromTemplate(menuTemplate) Menu.setApplicationMenu(menu) + } else { + tray = new Tray(path.join(__statics, "icon_32x32.png")) + const contextMenu = Menu.buildFromTemplate([ + { + label: "Show Arqma Wallet", + click: function() { + if(mainWindow.isMinimized()) + mainWindow.minimize() + else + mainWindow.show() + mainWindow.focus() + } + }, + { + label: "Exit Arqma Wallet", + click: function() { + if(mainWindow.isMinimized()) + mainWindow.minimize() + else + mainWindow.show() + mainWindow.focus() + mainWindow.close() + } + } + ]) + + tray.setContextMenu(contextMenu) + + updateTrayInterval = setInterval(() => { + if (backend) + tray.setToolTip(backend.getTooltipLabel()) + }, 1000) + + tray.on("click", () => { + if(mainWindow.isMinimized()) + mainWindow.minimize() + else + mainWindow.show() + mainWindow.focus() + }) } + createWindow() }) @@ -187,7 +208,7 @@ app.on("activate", () => { }) app.on("before-quit", () => { - // Quit instantly if we are installing an update + // Quit instantly if we are installing an update if (installUpdate) { return } @@ -195,6 +216,10 @@ app.on("before-quit", () => { forceQuit = true } else { if (backend) { + if (process.platform !== "darwin") { + clearInterval(updateTrayInterval) + tray.setToolTip("Closing...") + } backend.quit().then(() => { mainWindow.close() }) diff --git a/src-electron/main-process/modules/backend.js b/src-electron/main-process/modules/backend.js index 3498700..469c661 100644 --- a/src-electron/main-process/modules/backend.js +++ b/src-electron/main-process/modules/backend.js @@ -1,99 +1,154 @@ -import { Daemon } from "./daemon" -import { WalletRPC } from "./wallet-rpc" -import { Market } from "./market" -import { SCEE } from "./SCEE-Node" -import { dialog } from "electron" - -const WebSocket = require("ws") -const os = require("os") -const fs = require("fs-extra") -const path = require("path") -const objectAssignDeep = require("object-assign-deep") +import { Daemon } from "./daemon"; +import { WalletRPC } from "./wallet-rpc"; +import { Market } from "./market"; +import { Pool } from "./pool"; +import { ipcMain, dialog } from "electron"; + +const os = require("os"); +const fs = require("fs"); +const path = require("path"); +const objectAssignDeep = require("object-assign-deep"); + export class Backend { - constructor (mainWindow) { + constructor(mainWindow) { this.mainWindow = mainWindow this.daemon = null this.walletd = null this.market = null - this.wss = null - this.token = null + this.pool = null this.config_dir = null this.wallet_dir = null this.config_file = null this.config_data = {} - this.scee = new SCEE() } - init (config) { - if (os.platform() === "win32") { - this.config_dir = "C:\\ProgramData\\arqma" - this.wallet_dir = `${os.homedir()}\\Documents\\Arqma` + init() { + + if(os.platform() == "win32") { + this.config_dir = "C:\\ProgramData\\arqma"; + this.wallet_dir = `${os.homedir()}\\Documents\\Arqma` } else { - this.config_dir = path.join(os.homedir(), ".arqma") + this.config_dir = path.join(os.homedir(), ".arqma"); this.wallet_dir = path.join(os.homedir(), "Arqma") } if (!fs.existsSync(this.config_dir)) { - fs.mkdirpSync(this.config_dir) + fs.mkdirSync(this.config_dir); } - if (!fs.existsSync(path.join(this.config_dir, "gui"))) { - fs.mkdirpSync(path.join(this.config_dir, "gui")) + if (!fs.existsSync(path.join(this.config_dir, "gui"))) { + fs.mkdirSync(path.join(this.config_dir, "gui")); } this.config_file = path.join(this.config_dir, "gui", "config.json") - const daemon = { - type: "remote", - p2p_bind_ip: "0.0.0.0", - p2p_bind_port: 19993, - rpc_bind_ip: "127.0.0.1", - rpc_bind_port: 19994, - zmq_rpc_bind_ip: "127.0.0.1", - zmq_rpc_bind_port: 19995, - out_peers: -1, - in_peers: -1, - limit_rate_up: -1, - limit_rate_down: -1, - log_level: 0 - } + const daemon = { + type: "remote", + p2p_bind_ip: "0.0.0.0", + p2p_bind_port: 19993, + rpc_bind_ip: "127.0.0.1", + rpc_bind_port: 19994, + zmq_rpc_bind_ip: "127.0.0.1", + zmq_rpc_bind_port: 19995, + out_peers: -1, + in_peers: -1, + limit_rate_up: -1, + limit_rate_down: -1, + log_level: 0 + } - const daemons = { - mainnet: { - ...daemon, + const daemons = { + mainnet: { + ...daemon, + remote_host: "node.supportarqma.com", + remote_port: 19994 + }, + stagenet: { + ...daemon, + type: "local", + p2p_bind_port: 39993, + rpc_bind_port: 39994, + zmq_rpc_bind_port: 39995 + }, + testnet: { + ...daemon, + type: "local", + p2p_bind_port: 29993, + rpc_bind_port: 29994, + zmq_rpc_bind_port: 29995 + } + } + + // Default values + this.defaults = { + app: { + data_dir: this.config_dir, + ws_bind_port: 12213, + testnet: false, + wallet_data_dir: this.wallet_dir, + net_type: "mainnet", + wallet_dir: this.wallet_dir + }, + appearance: { + theme: "dark" + }, + + preference: { + notify_no_payment_id: true, + notify_empty_password: true, + minimize_to_tray: false, + autostart: false, + timeout: 600000 // 10 minutes + }, + daemon: { + type: "local_remote", remote_host: "node.supportarqma.com", - remote_port: 19994 - }, - stagenet: { - ...daemon, - type: "local", - p2p_bind_port: 39993, - rpc_bind_port: 39994, - zmq_rpc_bind_port: 39995 + remote_port: 19994, + p2p_bind_ip: "0.0.0.0", + p2p_bind_port: 19993, + rpc_bind_ip: "127.0.0.1", + rpc_bind_port: 19994, + zmq_rpc_bind_ip: "127.0.0.1", + zmq_rpc_bind_port: 19995, + out_peers: 8, + in_peers: 0, + limit_rate_up: -1, + limit_rate_down: -1, + log_level: 0, + enhanced_ip_privacy: true }, - testnet: { - ...daemon, - type: "local", - p2p_bind_port: 29993, - rpc_bind_port: 29994, - zmq_rpc_bind_port: 29995 - } - } - // Default values - this.defaults = { - daemons: objectAssignDeep({}, daemons), - app: { - data_dir: this.config_dir, - wallet_data_dir: this.wallet_dir, - ws_bind_port: 19994, - net_type: "mainnet" - }, wallet: { rpc_bind_port: 19999, log_level: 0 }, + + pool: { + server: { + enabled: false, + bindIP: "0.0.0.0", + bindPort: 3333, + }, + mining: { + address: "", + enableBlockRefreshInterval: false, + blockRefreshInterval: 5, + minerTimeout: 900, + uniform: true, + }, + varDiff: { + enabled: true, + startDiff: 5000, + minDiff: 1000, + maxDiff: 100000000, + targetTime: 30, + retargetTime: 60, + variancePercent: 30, + maxJump: 100, + fixedDiffSeparator: ".", + }, + }, market: { info: { default: 0, @@ -106,200 +161,170 @@ export class Backend { coin: "arqma", endpoint: "/api/v3/coins/arqma/tickers" } - } + }, + daemons: objectAssignDeep({}, daemons), } - this.config_data = { - // Copy all the properties of defaults - ...objectAssignDeep({}, this.defaults), - appearance: { - theme: "dark" - } - } + // Copy all the properties of defaults + ...objectAssignDeep({}, this.defaults), - this.remotes = [ - { - host: "node.supportarqma.com", - port: "19994" - } - ] + } - this.token = config.token + this.remotes = [ + { + host: "node.supportarqma.com", + port: "19994" + } + ] - this.wss = new WebSocket.Server({ - port: config.port, - maxPayload: Number.POSITIVE_INFINITY + ipcMain.on("event", (event, data) => { + this.receive(data) }) - this.wss.on("connection", ws => { - ws.on("message", data => this.receive(data)) - }) + this.startup() } - send (event, data = {}) { + send(event, data={}) { let message = { event, data } - let encrypted_data = this.scee.encryptString(JSON.stringify(message), this.token) - - this.wss.clients.forEach(function each (client) { - if (client.readyState === WebSocket.OPEN) { - client.send(encrypted_data) - } - }) + this.mainWindow.webContents.send("event", message) } - receive (data) { - let decrypted_data = JSON.parse(this.scee.decryptString(data, this.token)) + receive(data) { // route incoming request to either the daemon, wallet, or here - switch (decrypted_data.module) { - case "core": - this.handle(decrypted_data) - break - case "daemon": - if (this.daemon) { - this.daemon.handle(decrypted_data) - } - break - case "wallet": - if (this.walletd) { - this.walletd.handle(decrypted_data) - } - // eslint-disable-next-line no-fallthrough - case "market": - if (this.market) { - this.market.handle(decrypted_data) - } - break + switch (data.module) { + case "core": + this.handle(data); + break; + case "daemon": + if (this.daemon) { + this.daemon.handle(data); + } + break; + case "wallet": + if (this.walletd) { + this.walletd.handle(data); + } + if (this.market) { + this.market.handle(data) + } + break; + } } - } - handle (data) { + handle(data) { + let params = data.data switch (data.method) { - case "set_language": + case "set_language": this.send("set_language", { lang: params.lang }) break - case "quick_save_config": - // save only partial config settings - Object.keys(params).map(key => { - this.config_data[key] = Object.assign(this.config_data[key], params[key]) - }) - fs.writeFile(this.config_file, JSON.stringify(this.config_data, null, 4), "utf8", () => { - this.send("set_app_data", { - config: params, - pending_config: params - }) - }) - break - case "save_config": - // check if config has changed - let config_changed = false - Object.keys(this.config_data).map(i => { - if (i == "appearance") return - Object.keys(this.config_data[i]).map(j => { - if (this.config_data[i][j] !== params[i][j]) { config_changed = true } + case "quick_save_config": + // save only partial config settings + Object.keys(params).map(key => { + this.config_data[key] = Object.assign(this.config_data[key], params[key]) }) - }) - // eslint-disable-next-line no-fallthrough - case "save_config_init": - Object.keys(params).map(key => { - this.config_data[key] = Object.assign(this.config_data[key], params[key]) - }) - - const validated = Object.keys(this.defaults) - .filter(k => k in this.config_data) - .map(k => [k, this.validate_values(this.config_data[k], this.defaults[k])]) - .reduce((map, obj) => { - map[obj[0]] = obj[1] - return map - }, {}) - - // Validate deamon data - this.config_data = { - ...this.config_data, - ...validated - } - - fs.writeFile(this.config_file, JSON.stringify(this.config_data, null, 4), "utf8", () => { - if (data.method == "save_config_init") { - this.startup() - } else { + fs.writeFile(this.config_file, JSON.stringify(this.config_data, null, 4), 'utf8', () => { this.send("set_app_data", { - config: this.config_data, - pending_config: this.config_data + config: params, + pending_config: params }) - if (config_changed) { - this.send("settings_changed_reboot") - } - } - }) - break - case "init": - this.startup() - break - - case "open_explorer": - const { net_type } = this.config_data.app - - let path = null - if (params.type === "tx") { - path = "tx" - } else if (params.type === "service_node") { - path = "service_node" - } - - if (path) { - const baseUrl = net_type === "testnet" ? "https://stageblocks.arqma.com/" : "https://explorer.arqma.com/" - const url = `${baseUrl}/${path}/` - require("electron").shell.openExternal(url + params.id) - } - break - - case "open_url": - require("electron").shell.openExternal(params.url) - break - - case "save_png": - let filename = dialog.showSaveDialog(this.mainWindow, { - title: "Save " + params.type, - filters: [{ name: "PNG", extensions: ["png"] }], - defaultPath: os.homedir() - }) - if (filename) { - let base64Data = params.img.replace(/^data:image\/png;base64,/, "") - let binaryData = Buffer.from(base64Data, "base64").toString("binary") - fs.writeFile(filename, binaryData, "binary", (err) => { - if (err) { - this.send("show_notification", { - type: "negative", - i18n: ["notification.errors.errorSavingItem", { item: params.type }], - timeout: 2000 - }) + }) + break + + case "save_config": + // check if config has changed + let config_changed = false + Object.keys(this.config_data).map(i => { + if(i == "appearance" || i == "pool") return + Object.keys(this.config_data[i]).map(j => { + if(this.config_data[i][j] !== params[i][j]) + config_changed = true + }) + }) + case "save_config_init": + delete params.pool + Object.keys(params).map(key => { + this.config_data[key] = Object.assign(this.config_data[key], params[key]) + }); + fs.writeFile(this.config_file, JSON.stringify(this.config_data, null, 4), 'utf8', () => { + + if(data.method == "save_config_init") { + this.startup(); } else { - this.send("show_notification", { - i18n: ["notification.positive.itemSaved", { item: params.type, filename }], - timeout: 2000 + this.send("set_app_data", { + config: this.config_data, + pending_config: this.config_data, }) + if(config_changed) { + this.send("settings_changed_reboot") + } } + }); + break; + + case "save_pool_config": + Object.keys(params).map(key => { + this.config_data.pool[key] = Object.assign(this.config_data.pool[key], params[key]) }) - } - break + fs.writeFile(this.config_file, JSON.stringify(this.config_data, null, 4), 'utf8', () => { + this.send("set_app_data", { + config: this.config_data + }) + this.pool.init(this.config_data) + }) + break + + case "open_explorer": + const { net_type } = this.config_data.app + + let path = null + if (params.type === "tx") { + path = "tx" + } else if (params.type === "service_node") { + path = "service_node" + } + + if (path) { + const baseUrl = net_type === "testnet" ? "https://stageblocks.arqma.com/" : "https://explorer.arqma.com/" + const url = `${baseUrl}/${path}/` + require("electron").shell.openExternal(url + params.id) + } + break + + case "open_url": + require("electron").shell.openExternal(params.url) + break; + + case "save_png": + let filename = dialog.showSaveDialog(this.mainWindow, { + title: "Save "+params.type, + filters: [{name: "PNG", extensions:["png"]}], + defaultPath: os.homedir() + }) + if(filename) { + let base64Data = params.img.replace(/^data:image\/png;base64,/,"") + let binaryData = new Buffer(base64Data, 'base64').toString("binary") + fs.writeFile(filename, binaryData, "binary", (err) => { + if(err) + this.send("show_notification", {type: "negative", message: "Error saving "+params.type, timeout: 2000}) + else + this.send("show_notification", {message: params.type+" saved to "+filename, timeout: 2000}) + }) + } + break; - default: + default: } } - startup () { - this.send("set_app_data", { - remotes: this.remotes, - defaults: this.defaults - }) - + startup() { + this.send("initialize") fs.readFile(this.config_file, "utf8", (err, data) => { if (err) { this.send("set_app_data", { @@ -307,288 +332,373 @@ export class Backend { code: -1 // Config not found }, config: this.config_data, - pending_config: this.config_data - }) - return + pending_config: this.config_data, + }); + return; } - let disk_config_data = JSON.parse(data) + let disk_config_data = {} + try { + disk_config_data = JSON.parse(data) + } catch(e) { + } // semi-shallow object merge Object.keys(disk_config_data).map(key => { - if (!this.config_data.hasOwnProperty(key)) { this.config_data[key] = {} } + if(!this.config_data.hasOwnProperty(key)) + this.config_data[key] = {} this.config_data[key] = Object.assign(this.config_data[key], disk_config_data[key]) - }) + }); + + // If not Windows or Mac OS, and minimize to tray preference not set, force to false + // Else if is Windows or Mac OS, and minimize to tray preference not set, prevent minimize + if(this.config_data.preference.minimize_to_tray === null) { + if(os.platform() !== "win32" && os.platform() !== "darwin") { + this.config_data.preference.minimize_to_tray = false + } else { + this.mainWindow.setMinimizable(false) + } + } // here we may want to check if config data is valid, if not also send code -1 // i.e. check ports are integers and > 1024, check that data dir path exists, etc - const validated = Object.keys(this.defaults) - .filter(k => k in this.config_data) - .map(k => [k, this.validate_values(this.config_data[k], this.defaults[k])]) - .reduce((map, obj) => { - map[obj[0]] = obj[1] - return map - }, {}) - - // Make sure the daemon data is valid - this.config_data = { - ...this.config_data, - ...validated + + // Filter out http:// from remote_host (remote daemon address) + if(this.config_data.daemon.remote_host.indexOf("//") !== -1) { + let remote_host = this.config_data.daemon.remote_host + remote_host = this.config_data.daemon.remote_host.split("//") + remote_host.shift() + remote_host = remote_host.join("//") + this.config_data.daemon.remote_host = remote_host } // save config file back to file, so updated options are stored on disk - fs.writeFile(this.config_file, JSON.stringify(this.config_data, null, 4), "utf8", () => {}) + fs.writeFileSync(this.config_file, JSON.stringify(this.config_data, null, 4), "utf8"); - this.send("set_app_data", { - config: this.config_data, - pending_config: this.config_data - }) - // Make the wallet dir - const { wallet_data_dir, data_dir } = this.config_data.app - if (!fs.existsSync(wallet_data_dir)) { fs.mkdirpSync(wallet_data_dir) } - - // Check to see if data and wallet directories exist - const dirs_to_check = [{ - path: data_dir, - error: "notification.errors.dataPathNotFound" - }, - { - path: wallet_data_dir, - error: "notification.errors.walletPathNotFound" + // get network interfaces for UI + const interfaces = os.networkInterfaces() + let network_interfaces = [{ + value: "0.0.0.0", + label: "All interfaces - 0.0.0.0" }] - - for (const dir of dirs_to_check) { - // Check to see if dir exists - if (!fs.existsSync(dir.path)) { - this.send("show_notification", { - type: "negative", - i18n: dir.error, - timeout: 2000 - }) - - // Go back to config - this.send("set_app_data", { - status: { - code: -1 // Return to config screen - } - }) - return + for(let k in interfaces) { + for(let k2 in interfaces[k]) { + const address = interfaces[k][k2] + if(address.family === "IPv4" && address.internal) { + network_interfaces.push({ + value: address.address, + label: `Local machine only - ${address.address}` + }) + } else if(address.family === "IPv4" && !address.internal) { + network_interfaces.push({ + value: address.address, + label: `Local network only - ${address.address}` + }) + } } } - const { net_type } = this.config_data.app - const dirs = { - "mainnet": this.config_data.app.data_dir, - "stagenet": path.join(this.config_data.app.data_dir, "stagenet"), - "testnet": path.join(this.config_data.app.data_dir, "testnet") + this.send("set_app_data", { + config: this.config_data, + pending_config: this.config_data, + network_interfaces: network_interfaces + }); + + // Check to see if data dir exists, if not it may have been on network drive + // if not exist, send back to config screen with message so user can select + // new location + if (!fs.existsSync(this.config_data.app.data_dir)) { + this.send("show_notification", {type: "negative", message: "Error: data storge path not found", timeout: 2000}) + this.send("set_app_data", { + status: { + code: -1 // Return to config screen + } + }); + return; } - // Make sure we have the directories we need - const net_dir = dirs[net_type] - if (!fs.existsSync(net_dir)) { fs.mkdirpSync(net_dir) } + let lmdb_dir = path.join(this.config_data.app.data_dir, "lmdb02") + let log_dir = path.join(this.config_data.app.data_dir, "logs") + let wallet_dir = path.join(this.config_data.app.data_dir, "wallets") + let gui_dir = path.join(this.config_data.app.data_dir, "gui") - const log_dir = path.join(net_dir, "logs") - if (!fs.existsSync(log_dir)) { fs.mkdirpSync(log_dir) } + if(this.config_data.app.testnet) { - this.daemon = new Daemon(this) - this.walletd = new WalletRPC(this) - this.market = new Market(this) + let testnet_dir = path.join(this.config_data.app.data_dir, "testnet") + if (!fs.existsSync(testnet_dir)) + fs.mkdirSync(testnet_dir); - this.send("set_app_data", { - status: { - code: 3 // Starting daemon - } - }) + lmdb_dir = path.join(testnet_dir, "lmdb02") + log_dir = path.join(testnet_dir, "logs") + wallet_dir = path.join(testnet_dir, "wallets") + gui_dir = path.join(testnet_dir, "gui") - // Make sure the remote node provided is accessible - const config_daemon = this.config_data.daemons[net_type] - this.daemon.checkRemote(config_daemon).then(data => { - if (data.error) { - // If we can default to local then we do so, otherwise we tell the user to re-set the node - if (config_daemon.type === "local_remote") { - this.config_data.daemons[net_type].type = "local" - this.send("set_app_data", { - config: this.config_data, - pending_config: this.config_data - }) - this.send("show_notification", { - type: "warning", - textColor: "black", - i18n: "notification.warnings.usingLocalNode", - timeout: 2000 - }) - } else { - this.send("show_notification", { - type: "negative", - i18n: "notification.errors.cannotAccessRemoteNode", - timeout: 2000 - }) + } - // Go back to config - this.send("set_app_data", { - status: { - code: -1 // Return to config screen - } - }) - return + if (!fs.existsSync(lmdb_dir)) + fs.mkdirSync(lmdb_dir); + if (!fs.existsSync(log_dir)) + fs.mkdirSync(log_dir); + if (!fs.existsSync(wallet_dir)) + fs.mkdirSync(wallet_dir) + if (!fs.existsSync(gui_dir)) + fs.mkdirSync(gui_dir) + + // Check permissions + try { + fs.accessSync(this.config_dir, fs.constants.R_OK | fs.constants.W_OK); + fs.accessSync(path.join(this.config_dir, "gui"), fs.constants.R_OK | fs.constants.W_OK); + fs.accessSync(this.config_file, fs.constants.R_OK | fs.constants.W_OK); + fs.accessSync(lmdb_dir, fs.constants.R_OK | fs.constants.W_OK); + fs.accessSync(log_dir, fs.constants.R_OK | fs.constants.W_OK); + fs.accessSync(wallet_dir, fs.constants.R_OK | fs.constants.W_OK); + } catch (err) { + this.send("show_notification", {type: "negative", message: "Error: data storge path not writable", timeout: 2000}) + this.send("set_app_data", { + status: { + code: -1 // Return to config screen } + }); + return; + } + const { net_type } = this.config_data.app + + this.daemon = new Daemon(this); + this.walletd = new WalletRPC(this); + this.pool = new Pool(this); + this.market = new Market(this); + + this.send("set_app_data", { + status: { + code: 3 // Starting daemon } + }); - // If we got a net type back then check if ours match - if (data.net_type && data.net_type !== net_type) { - this.send("show_notification", { - type: "negative", - i18n: "notification.errors.differentNetType", - timeout: 2000 - }) + this.daemon.checkVersion().then((version) => { - // Go back to config + if(version) { this.send("set_app_data", { status: { - code: -1 // Return to config screen + code: 4, + message: version } - }) - return + }); + } else { + // daemon not found, probably removed by AV, set to remote node + this.config_data.daemon.type = "remote" + this.send("set_app_data", { + config: this.config_data, + pending_config: this.config_data, + }); + this.send("show_notification", {type: "warning", textColor: "black", message: "Warning: arqmad not found, using remote node", timeout: 2000}) } - this.daemon.checkVersion().then((version) => { - if (version) { - this.send("set_app_data", { - status: { - code: 4, - message: version - } - }) - } else { - // daemon not found, probably removed by AV, set to remote node - this.config_data.daemons[net_type].type = "remote" - this.send("set_app_data", { - status: { - code: 5 - }, - config: this.config_data, - pending_config: this.config_data - }) + this.market.start(this.config_data) + .then(() => { + + }) + .catch(error => { + + }) + this.daemon.checkRemoteDaemon(this.config_data).then((data) => { + + if(data.hasOwnProperty("error")) { + // error contacting remote daemon + + if(this.config_data.daemon.type == "local_remote") { + // if in local+remote, then switch to local only + this.config_data.daemon.type = "local" + this.send("set_app_data", { + config: this.config_data, + pending_config: this.config_data, + }); + this.send("show_notification", {type: "warning", textColor: "black", message: "Warning: remote node not available, using local node", timeout: 2000}) + + } else if(this.config_data.daemon.type == "remote") { + this.send("set_app_data", { + status: { + code: -1 // Return to config screen + } + }); + this.send("show_notification", {type: "negative", message: "Error: remote node not available, change to local mode or update remote node", timeout: 2000}) + return; + } + } else if(this.config_data.app.testnet && !data.result.testnet) { + // remote node network does not match local network (testnet, mainnet) + + if(this.config_data.daemon.type == "local_remote") { + // if in local+remote, then switch to local only + this.config_data.daemon.type = "local" + this.send("set_app_data", { + config: this.config_data, + pending_config: this.config_data, + }); + this.send("show_notification", {type: "warning", textColor: "black", message: "Warning: remote node network does not match, using local node", timeout: 2000}) + + } else if(this.config_data.daemon.type == "remote") { + this.send("set_app_data", { + status: { + code: -1 // Return to config screen + } + }); + this.send("show_notification", {type: "negative", message: "Error: remote node network does not match, change to local mode or update remote node", timeout: 2000}) + return; + } + } this.daemon.start(this.config_data).then(() => { + this.send("set_app_data", { status: { code: 6 // Starting wallet } - }) + }); this.walletd.start(this.config_data).then(() => { + this.send("set_app_data", { status: { code: 7 // Reading wallet list } - }) + }); this.walletd.listWallets(true) + this.pool.init(this.config_data) + this.send("set_app_data", { status: { code: 0 // Ready } - }) - // eslint-disable-next-line + }); }).catch(error => { - this.daemon.killProcess() - this.send("show_notification", { type: "negative", message: error.message, timeout: 3000 }) this.send("set_app_data", { status: { code: -1 // Return to config screen } - }) - }) - // eslint-disable-next-line + }); + return; + }); + }).catch(error => { - if (this.config_data.daemons[net_type].type == "remote") { - this.send("show_notification", { - type: "negative", - i18n: "notification.errors.remoteCannotBeReached", - timeout: 3000 - }) + if(this.config_data.daemon.type == "remote") { + this.send("show_notification", {type: "negative", message: "Remote daemon cannot be reached", timeout: 2000}) } else { - this.send("show_notification", { - type: "negative", - message: error.message, - timeout: 3000 - }) + this.send("show_notification", {type: "negative", message: "Local daemon internal error", timeout: 2000}) } this.send("set_app_data", { status: { code: -1 // Return to config screen } - }) - }) - // eslint-disable-next-line - }).catch(error => { - this.send("set_app_data", { - status: { - code: -1 // Return to config screen - } - }) - }) - }) + }); + return; + }); + }); + }).catch(error => { + this.send("set_app_data", { + status: { + code: -1 // Return to config screen + } + }); + return; + }); this.market.start(this.config_data) - .then(() => { + .then(() => { + }) + .catch(error => { + }) + }); + } - }) - .catch(error => { + getTooltipLabel () { - }) - }) - } + if(!this.daemon || !this.walletd) + return "Initializing..." - quit () { - return new Promise((resolve, reject) => { - let process = [] - if (this.daemon) { process.push(this.daemon.quit()) } - if (this.walletd) { process.push(this.walletd.quit()) } - if (this.market) { process.push(this.market.quit()) } - if (this.wss) { this.wss.close() } + let daemon_type = this.config_data.daemon.type + let daemon_info = this.daemon.daemon_info + let wallet_info = this.walletd.wallet_info - Promise.all(process).then(() => { - resolve() - }) - }) - } + if(Object.keys(daemon_info).length == 0 || Object.keys(wallet_info).length == 0) + return "Initializing..." - // Replace any invalid value with default values - validate_values (values, defaults) { - const isDictionary = (v) => typeof v === "object" && v !== null && !(v instanceof Array) && !(v instanceof Date) - const modified = { ...values } + let target_height = 0 + if(daemon_type === "local" && !daemon_info.is_ready) + target_height = Math.max(daemon_info.height, daemon_info.target_height) + else + target_height = daemon_info.height - // Make sure we have valid defaults - if (!isDictionary(defaults)) return modified + let daemon_local_pct = 0 + if(daemon_type !== "remote") { + let d_pct = (100 * daemon_info.height_without_bootstrap / target_height).toFixed(1) + if(d_pct == 100.0 && daemon_info.height_without_bootstrap < target_height) + daemon_local_pct = 99.9 + else + daemon_local_pct = d_pct + } - for (const key in modified) { - // Only modify if we have a default - if (!(key in defaults)) continue + let daemon_pct = 0 + if(daemon_type === "local") + daemon_pct = daemon_local_pct - const defaultValue = defaults[key] - const invalidDefault = defaultValue === null || defaultValue === undefined || Number.isNaN(defaultValue) - if (invalidDefault) continue + let wallet_pct = 0 + let w_pct = (100 * wallet_info.height / target_height).toFixed(1) + if(w_pct == 100.0 && wallet_info.height < target_height) + wallet_pct = 99.9 + else + wallet_pct = w_pct - const value = modified[key] + let status = "" - // If we have a object then recurse through it - if (isDictionary(value)) { - modified[key] = this.validate_values(value, defaultValue) - } else { - // Check if we need to replace the value - const isValidValue = !(value === undefined || value === null || value === "" || Number.isNaN(value)) - if (isValidValue) continue + if(daemon_type !== "remote") { + status += `Daemon: ${daemon_info.height_without_bootstrap} / ${target_height} (${daemon_local_pct}%) ` + } + + if(daemon_type !== "local") { + status += `Remote: ${daemon_info.height} ` + } - // Otherwise set the default value - modified[key] = defaultValue + status += `Wallet: ${wallet_info.height} / ${target_height} (${wallet_pct}%) ` + + if(daemon_type === "local") { + if(daemon_info.height_without_bootstrap < target_height || !daemon_info.is_ready) { + status += "Syncing..." + } else if(wallet_info.height < target_height - 1 && wallet_info.height != 0) { + status += "Scanning..." + } else { + status += "Ready" + } + } else { + if(wallet_info.height < target_height - 1 && wallet_info.height != 0) { + status += "Scanning..." + } else if(daemon_info.height_without_bootstrap < target_height) { + status += "Syncing..." + } else { + status += "Ready" } } - return modified + + return status + } + + quit() { + return new Promise((resolve, reject) => { + let process = [] + if(this.daemon) + process.push(this.daemon.quit()) + if(this.walletd) + process.push(this.walletd.quit()) + if(this.pool) + process.push(this.pool.quit()) + if(this.market) + process.push(this.market.quit()) + Promise.all(process).then(() => { + resolve() + }) + }) } } diff --git a/src-electron/main-process/modules/daemon.js b/src-electron/main-process/modules/daemon.js index b9fc775..45f36eb 100644 --- a/src-electron/main-process/modules/daemon.js +++ b/src-electron/main-process/modules/daemon.js @@ -1,85 +1,87 @@ -import child_process from "child_process" -const request = require("request-promise") -const queue = require("promise-queue") -const http = require("http") -const fs = require("fs") -const path = require("path") -const portscanner = require("portscanner") +import child_process from "child_process"; +const request = require("request-promise"); +const queue = require("promise-queue"); +const http = require("http"); +const fs = require("fs"); +const path = require("path"); export class Daemon { - constructor (backend) { + constructor(backend) { this.backend = backend this.heartbeat = null this.heartbeat_slow = null this.id = 0 - this.net_type = "mainnet" + this.testnet = false this.local = false // do we have a local daemon ? - this.agent = new http.Agent({ keepAlive: true, maxSockets: 1 }) + this.daemon_info = {} + + this.agent = new http.Agent({keepAlive: true, maxSockets: 1}) this.queue = new queue(1, Infinity) - // Settings for timestamp to height conversion - // These are initial values used to calculate the height - this.PIVOT_BLOCK_HEIGHT = 150000 - this.PIVOT_BLOCK_TIMESTAMP = 1554667008 - this.PIVOT_BLOCK_TIME = 120 } - checkVersion () { + + checkVersion() { return new Promise((resolve, reject) => { if (process.platform === "win32") { - // eslint-disable-next-line no-undef let arqmad_path = path.join(__arqma_bin, "arqmad.exe") let arqmad_version_cmd = `"${arqmad_path}" --version` - if (!fs.existsSync(arqmad_path)) { resolve(false) } + if (!fs.existsSync(arqmad_path)) + resolve(false) child_process.exec(arqmad_version_cmd, (error, stdout, stderr) => { - if (error) { resolve(false) } + if(error) + resolve(false) resolve(stdout) }) } else { - // eslint-disable-next-line no-undef let arqmad_path = path.join(__arqma_bin, "arqmad") let arqmad_version_cmd = `"${arqmad_path}" --version` - if (!fs.existsSync(arqmad_path)) { resolve(false) } - child_process.exec(arqmad_version_cmd, { detached: true }, (error, stdout, stderr) => { - if (error) { resolve(false) } + if (!fs.existsSync(arqmad_path)) + resolve(false) + child_process.exec(arqmad_version_cmd, {detached: true}, (error, stdout, stderr) => { + if(error) + resolve(false) resolve(stdout) }) } }) } - checkRemote (daemon) { - if (daemon.type === "local") { - return Promise.resolve({}) + checkRemoteDaemon(options) { + if(options.daemon.type == "local") { + return new Promise((resolve, reject) => { + resolve({ + result: { + mainnet: !options.app.testnet, + testnet: options.app.testnet, + } + }) + }) + } else { + let uri = `http://${options.daemon.remote_host}:${options.daemon.remote_port}/json_rpc` + return new Promise((resolve, reject) => { + this.sendRPC("get_info", {}, uri).then((data) => { + resolve(data) + }) + }) } - - return this.sendRPC("get_info", {}, { - protocol: "http://", - hostname: daemon.remote_host, - port: daemon.remote_port - }).then(data => { - if (data.error) return { error: data.error } - return { - net_type: data.result.nettype - } - }) } - start (options) { - const { net_type } = options.app - const daemon = options.daemons[net_type] - if (daemon.type === "remote") { + start(options) { + + if(options.daemon.type === "remote") { + this.local = false // save this info for later RPC calls this.protocol = "http://" - this.hostname = daemon.remote_host - this.port = daemon.remote_port + this.hostname = options.daemon.remote_host + this.port = options.daemon.remote_port return new Promise((resolve, reject) => { this.sendRPC("get_info").then((data) => { - if (!data.hasOwnProperty("error")) { + if(!data.hasOwnProperty("error")) { this.startHeartbeat() resolve() } else { @@ -89,125 +91,113 @@ export class Daemon { }) } return new Promise((resolve, reject) => { + this.local = true const args = [ "--data-dir", options.app.data_dir, - "--p2p-bind-ip", daemon.p2p_bind_ip, - "--p2p-bind-port", daemon.p2p_bind_port, - "--rpc-bind-ip", daemon.rpc_bind_ip, - "--rpc-bind-port", daemon.rpc_bind_port, - "--zmq-rpc-bind-ip", daemon.zmq_rpc_bind_ip, - "--zmq-rpc-bind-port", daemon.zmq_rpc_bind_port, - "--out-peers", daemon.out_peers, - "--in-peers", daemon.in_peers, - "--limit-rate-up", daemon.limit_rate_up, - "--limit-rate-down", daemon.limit_rate_down, - "--log-level", daemon.log_level - ] - - const dirs = { - "mainnet": options.app.data_dir, - "stagenet": path.join(options.app.data_dir, "stagenet"), - "testnet": path.join(options.app.data_dir, "testnet") + "--rpc-bind-ip", options.daemon.rpc_bind_ip, + "--rpc-bind-port", options.daemon.rpc_bind_port, + "--zmq-rpc-bind-ip", options.daemon.zmq_rpc_bind_ip, + "--zmq-rpc-bind-port", options.daemon.zmq_rpc_bind_port, + "--out-peers", options.daemon.out_peers, + "--in-peers", options.daemon.in_peers, + "--limit-rate-up", options.daemon.limit_rate_up, + "--limit-rate-down", options.daemon.limit_rate_down, + "--log-level", options.daemon.log_level, + ]; + + if(options.daemon.enhanced_ip_privacy) { + args.push( + "--p2p-bind-ip", "127.0.0.1", + "--p2p-bind-port", options.daemon.p2p_bind_port, + "--no-igd", + "--hide-my-port" + ) + } else { + args.push( + "--p2p-bind-ip", options.daemon.p2p_bind_ip, + "--p2p-bind-port", options.daemon.p2p_bind_port + ) } - const { net_type } = options.app - this.net_type = net_type - - if (net_type === "testnet") { + if(options.app.testnet) { + this.testnet = true args.push("--testnet") - } else if (net_type === "stagenet") { - args.push("--stagenet") + args.push("--log-file", path.join(options.app.data_dir, "testnet", "logs", "arqmad.log")) + //args.push("--add-peer", "45.77.68.151:13310") + } else { + args.push("--log-file", path.join(options.app.data_dir, "logs", "arqmad.log")) } - args.push("--log-file", path.join(dirs[net_type], "logs", "arqmad.log")) - - if (daemon.rpc_bind_ip !== "127.0.0.1") { args.push("--confirm-external-bind") } + if(options.daemon.rpc_bind_ip !== "127.0.0.1") + args.push("--confirm-external-bind") - // TODO: Check if we need to push this command for staging too - if (daemon.type === "local_remote" && net_type === "mainnet") { + if(options.daemon.type === "local_remote" && !options.app.testnet) { args.push( "--bootstrap-daemon-address", - `${daemon.remote_host}:${daemon.remote_port}` + `${options.daemon.remote_host}:${options.daemon.remote_port}` ) } + if (process.platform === "win32") { + this.daemonProcess = child_process.spawn(path.join(__arqma_bin, "arqmad.exe"), args) + } else { + this.daemonProcess = child_process.spawn(path.join(__arqma_bin, "arqmad"), args, { + detached: true + }) + } + // save this info for later RPC calls this.protocol = "http://" - this.hostname = daemon.rpc_bind_ip - this.port = daemon.rpc_bind_port - - portscanner.checkPortStatus(this.port, this.hostname).catch(e => "closed").then(status => { - if (status === "closed") { - if (process.platform === "win32") { - // eslint-disable-next-line no-undef - this.daemonProcess = child_process.spawn(path.join(__arqma_bin, "arqmad.exe"), args) - } else { - // eslint-disable-next-line no-undef - this.daemonProcess = child_process.spawn(path.join(__arqma_bin, "arqmad"), args, { - detached: true - }) - } + this.hostname = options.daemon.rpc_bind_ip + this.port = options.daemon.rpc_bind_port + + this.daemonProcess.stdout.on("data", data => process.stdout.write(`Daemon: ${data}`)) + this.daemonProcess.on("error", err => process.stderr.write(`Daemon: ${err}\n`)) + this.daemonProcess.on("close", code => process.stderr.write(`Daemon: exited with code ${code}\n`)) - this.daemonProcess.stdout.on("data", data => process.stdout.write(`Daemon: ${data}`)) - this.daemonProcess.on("error", err => process.stderr.write(`Daemon: ${err}`)) - this.daemonProcess.on("close", code => { - process.stderr.write(`Daemon: exited with code ${code} \n`) - this.daemonProcess = null - this.agent.destroy() - if (code === null) { - reject(new Error("Failed to start local daemon")) + // To let caller know when the daemon is ready + let intrvl = setInterval(() => { + this.sendRPC("get_info").then((data) => { + if(!data.hasOwnProperty("error")) { + this.startHeartbeat() + clearInterval(intrvl); + resolve(); + } else { + if(data.error.cause && + data.error.cause.code === "ECONNREFUSED") { + // Ignore + } else { + clearInterval(intrvl); + reject(error); } - }) - - // To let caller know when the daemon is ready - let intrvl = setInterval(() => { - this.sendRPC("get_info").then((data) => { - if (!data.hasOwnProperty("error")) { - this.startHeartbeat() - clearInterval(intrvl) - resolve() - } else { - if (data.error.cause && - data.error.cause.code === "ECONNREFUSED") { - // Ignore - } else { - clearInterval(intrvl) - this.killProcess() - reject(new Error("Could not connect to local daemon")) - } - } - }) - }, 1000) - } else { - reject(new Error(`Local daemon port ${this.port} is in use`)) - } - }) + } + }) + }, 1000) }) } - killProcess () { - if (this.daemonProcess) { - this.daemonProcess.kill() - this.daemonProcess = null - } - } + handle(data) { - handle (data) { let params = data.data switch (data.method) { - case "ban_peer": - this.banPeer(params.host, params.seconds) - break - default: + case "ban_peer": + this.banPeer(params.host, params.seconds) + break + + default: + } + } - banPeer (host, seconds = 3600) { - if (!seconds) { seconds = 3600 } + banPeer(host, seconds=3600) { + + if(!seconds) + seconds=3600 let params = { bans: [{ @@ -218,52 +208,51 @@ export class Daemon { } this.sendRPC("set_bans", params).then((data) => { - if (data.hasOwnProperty("error") || !data.hasOwnProperty("result")) { - this.sendGateway("show_notification", { - type: "negative", - i18n: "notification.errors.banningPeer", - timeout: 2000 - }) + if(data.hasOwnProperty("error") || !data.hasOwnProperty("result")) { + this.sendGateway("show_notification", {type: "negative", message: "Error banning peer", timeout: 2000}) return } let end_time = new Date(Date.now() + seconds * 1000).toLocaleString() - this.sendGateway("show_notification", { - i18n: ["notification.positive.bannedPeer", { host, time: end_time }], - timeout: 2000 - }) + this.sendGateway("show_notification", {message: "Banned "+host+" until "+end_time, timeout: 2000}) // Send updated peer and ban list this.heartbeatSlowAction() + }) + } - timestampToHeight (timestamp, pivot = null, recursion_limit = null) { + timestampToHeight(timestamp, pivot=null, recursion_limit=null) { + return new Promise((resolve, reject) => { - if (timestamp > 999999999999) { + + if(timestamp > 999999999999) { // We have got a JS ms timestamp, convert timestamp = Math.floor(timestamp / 1000) } - pivot = pivot || [this.PIVOT_BLOCK_HEIGHT, this.PIVOT_BLOCK_TIMESTAMP] - recursion_limit = recursion_limit || 0 + pivot = pivot || [137500, 1528073506] + recursion_limit = recursion_limit || 0; - let diff = Math.floor((timestamp - pivot[1]) / this.PIVOT_BLOCK_TIME) + let diff = Math.floor((timestamp - pivot[1]) / 120) let estimated_height = pivot[0] + diff - if (estimated_height <= 0) { + if(estimated_height <= 0) { return resolve(0) } - if (recursion_limit > 10) { + if(recursion_limit > 10) { return resolve(pivot[0]) } - this.getRPC("block_header_by_height", { height: estimated_height }).then((data) => { - if (data.hasOwnProperty("error") || !data.hasOwnProperty("result")) { - if (data.error.code == -2) { // Too big height + this.getRPC("block_header_by_height", {height: estimated_height}).then((data) => { + + if(data.hasOwnProperty("error") || !data.hasOwnProperty("result")) { + if(data.error.code == -2) { // Too big height + this.getRPC("last_block_header").then((data) => { - if (data.hasOwnProperty("error") || !data.hasOwnProperty("result")) { + if(data.hasOwnProperty("error") || !data.hasOwnProperty("result")) { return reject() } @@ -272,7 +261,7 @@ export class Daemon { // If we are within an hour that is good enough // If for some reason there is a > 1h gap between blocks // the recursion limit will take care of infinite loop - if (Math.abs(timestamp - new_pivot[1]) < 3600) { + if(Math.abs(timestamp - new_pivot[1]) < 3600) { return resolve(new_pivot[0]) } @@ -290,23 +279,26 @@ export class Daemon { // If we are within an hour that is good enough // If for some reason there is a > 1h gap between blocks // the recursion limit will take care of infinite loop - if (Math.abs(timestamp - new_pivot[1]) < 3600) { + if(Math.abs(timestamp - new_pivot[1]) < 3600) { return resolve(new_pivot[0]) } // Continue recursion with new pivot resolve(new_pivot) + }) }).then((pivot_or_height) => { + return Array.isArray(pivot_or_height) ? this.timestampToHeight(timestamp, pivot_or_height, recursion_limit + 1) : pivot_or_height - }).catch(e => { + + }).catch(error => { return false }) } - startHeartbeat () { + startHeartbeat() { clearInterval(this.heartbeat) this.heartbeat = setInterval(() => { this.heartbeatAction() @@ -319,18 +311,13 @@ export class Daemon { }, 30 * 1000) // 30 seconds this.heartbeatSlowAction() - clearInterval(this.serviceNodeHeartbeat) - this.serviceNodeHeartbeat = setInterval(() => { - this.updateServiceNodes() - }, 5 * 60 * 1000) // 5 minutes - this.updateServiceNodes() } - heartbeatAction () { + heartbeatAction() { let actions = [] // No difference between local and remote heartbeat action for now - if (this.local) { + if(this.local) { actions = [ this.getRPC("info") ] @@ -344,36 +331,39 @@ export class Daemon { let daemon_info = { } for (let n of data) { - if (n == undefined || !n.hasOwnProperty("result") || n.result == undefined) { continue } - if (n.method == "get_info") { + if(n == undefined || !n.hasOwnProperty("result") || n.result == undefined) + continue + if(n.method == "get_info") { daemon_info.info = n.result + this.daemon_info = n.result } } this.sendGateway("set_daemon_data", daemon_info) }) } - heartbeatSlowAction () { + heartbeatSlowAction() { let actions = [] - if (this.local) { + if(this.local) { actions = [ this.getRPC("connections"), - this.getRPC("bans") - // this.getRPC("txpool_backlog"), + this.getRPC("bans"), + //this.getRPC("txpool_backlog"), ] } else { actions = [ - // this.getRPC("txpool_backlog"), + //this.getRPC("txpool_backlog"), ] } - if (actions.length === 0) return + if(actions.length === 0) return Promise.all(actions).then((data) => { let daemon_info = { } for (let n of data) { - if (n == undefined || !n.hasOwnProperty("result") || n.result == undefined) { continue } + if(n == undefined || !n.hasOwnProperty("result") || n.result == undefined) + continue if (n.method == "get_connections" && n.result.hasOwnProperty("connections")) { daemon_info.connections = n.result.connections } else if (n.method == "get_bans" && n.result.hasOwnProperty("bans")) { @@ -386,35 +376,14 @@ export class Daemon { }) } - updateServiceNodes () { - // Get the latest service node data - this.getRPC("service_nodes").then(data => { - if (!data.hasOwnProperty("result")) return - - const states = data.result.service_node_states - - // Only store the data we need - const service_nodes = states.map(s => ({ - service_node_pubkey: s.service_node_pubkey, - contributors: s.contributors - })) - this.sendGateway("set_daemon_data", { service_nodes }) - }) - } - - sendGateway (method, data) { + sendGateway(method, data) { this.backend.send(method, data) } - sendRPC (method, params = {}, options = {}) { + sendRPC(method, params={}, uri=false) { let id = this.id++ - - const protocol = options.protocol || this.protocol - const hostname = options.hostname || this.hostname - const port = options.port || this.port - - let requestOptions = { - uri: `${protocol}${hostname}:${port}/json_rpc`, + let options = { + uri: uri ? uri : `${this.protocol}${this.hostname}:${this.port}/json_rpc`, method: "POST", json: { jsonrpc: "2.0", @@ -422,15 +391,14 @@ export class Daemon { method: method }, agent: this.agent + }; + if(Array.isArray(params) || Object.keys(params).length !== 0) { + options.json.params = params } - if (Object.keys(params).length !== 0) { - requestOptions.json.params = params - } - return this.queue.add(() => { - return request(requestOptions) + return request(options) .then((response) => { - if (response.hasOwnProperty("error")) { + if(response.hasOwnProperty("error")) { return { method: method, params: params, @@ -459,27 +427,21 @@ export class Daemon { /** * Call one of the get_* RPC calls */ - getRPC (parameter, args) { - return this.sendRPC(`get_${parameter}`, args) + getRPC(parameter, args) { + return this.sendRPC(`get_${parameter}`, args); } - quit () { - clearInterval(this.heartbeat) + quit() { + // TODO force close after few seconds! + clearInterval(this.heartbeat); + this.queue.queue = [] return new Promise((resolve, reject) => { if (this.daemonProcess) { this.daemonProcess.on("close", code => { this.agent.destroy() - clearTimeout(this.forceKill) resolve() }) - - // Force kill after 20 seconds - this.forceKill = setTimeout(() => { - this.daemonProcess.kill("SIGKILL") - }, 20000) - - const signal = this.isDaemonSyncing ? "SIGKILL" : "SIGTERM" - this.daemonProcess.kill(signal) + this.daemonProcess.kill() } else { resolve() } diff --git a/src-electron/main-process/modules/market.js b/src-electron/main-process/modules/market.js index 106d60a..c5c6443 100644 --- a/src-electron/main-process/modules/market.js +++ b/src-electron/main-process/modules/market.js @@ -65,7 +65,7 @@ export class Market { let label = `${key} ${symbol}` let price = +ticker.last if (price === 0) continue - data.push({ key: key, label: label, symbol: symbol, value: price }) + data.push({ key: label, label: label, symbol: symbol, value: price }) } this.sendGateway("set_market_data", { info: { exchanges: data } }) } catch (error) {} diff --git a/src-electron/main-process/modules/pool.js b/src-electron/main-process/modules/pool.js new file mode 100644 index 0000000..fb4a966 --- /dev/null +++ b/src-electron/main-process/modules/pool.js @@ -0,0 +1,718 @@ +import ryo_utils_promise from "ryo-core-js/ryo_utils/ryo_utils" +import * as ryo_utils_nettype from "ryo-core-js/ryo_utils/ryo_utils_nettype" +import BigInt from "big-integer" +import { createServer } from "net" +import { join } from "path" +import { Miner } from "./pool/miner" +import { Block } from "./pool/block" +import { Database } from "./pool/database" +import { diff1, noncePattern, uid, logger } from "./pool/utils" + +const request = require("request-promise") +const http = require("http") + +export class Pool { + constructor(backend) { + this.backend = backend + this.daemon_type = backend.config_data.daemon.type + this.active = false + this.config = null + this.server = null + + this.id = 0 + this.agent = new http.Agent({keepAlive: true, maxSockets: 1}) + + this.intervals = { + startup: null, + job: null, + timeout: null, + watchdog: null, + retarget: null, + stats: null + } + + const { data_dir, testnet } = backend.config_data.app + + this.testnet = testnet + + if(testnet) { + this.nettype = ryo_utils_nettype.network_type.TESTNET + logger.setLogFile(join(data_dir, "testnet", "logs"), "pool.log") + } else { + this.nettype = ryo_utils_nettype.network_type.MAINNET + logger.setLogFile(join(data_dir, "logs"), "pool.log") + } + logger.log("info", "Logger initialized") + + this.database = new Database(this, { testnet, data_dir }) + + ryo_utils_promise.then(core_bridge => { + this.core_bridge = core_bridge + + // Initial check to make sure construct block blob is working with known values + // const template = "0707a3b8a9e5050b20eb040be32fe62a3c0b539df3732d469915e05d4a2a5b251c6ea3cd028f010000000003ff880f01ffc3880f01a0f2afc7b50102e4125c8981676668d8fe7761d8daad48bb9295244dab1bbd26e71f5789b48df824010b83b6cf1057efa44dcebf987a7b7b9fb6d0618adb87f8a5f28aef25f173cfe902010400073b91d442653cb9cfa2df6429e98bbeff3124fe4074c4f92529480d95e83c3cf6e32a3ae16c9e1e1aa6a3a4a61f16d16f6a419db7532dba87d6121b9473c84e271a2bf05f0ee7610febb915d0c6eed95f804ad90d8cd97a6e39360fc0350170fc8ae65fcdf6ca2cb94303ba60e923719436a67f9f0b93bf1cec0034276b934ef5a874b47e40aa38dd545a692960a6a88f98e164c9b926f9c5aa7a44803491ae6cdc6874d3ad893e10f9aeb2c8ca350679e8ef7c9cebc68124b33206bdf8f53fd6b9760514951531d208c8b6e293836694f68547f3dd1a25e0f39ea5140681f05a" + // const nonce = "7ec9adaa" + // let block_blob = "" + // try { + // block_blob = this.core_bridge.construct_block_blob(template, Buffer.from(nonce, "hex").readUInt32LE(0)) + // logger.log("info", "Initial block blob okay") + // } catch(error) { + // logger.log("error", "Initial block blob error") + // logger.log("error", error) + // } + + }) + + this.checkHeight().then(response => { + logger.log("info", "Contacted remote API -- watchdog is active") + logger.log("info", response) + }).catch(() => { + logger.log("warn", "Could not contact remote API") + }) + + } + + init(options) { + if(this.daemon_type == "remote") { + return false + } + + this.protocol = "http://" + this.hostname = options.daemon.rpc_bind_ip + this.port = options.daemon.rpc_bind_port + + try { + + if(this.database.db == null) { + this.database.start() + logger.log("info", "Database initialized") + } + + if(this.intervals.stats == null) { + this.statsHeartbeat() + this.intervals.stats = setInterval(() => { + this.statsHeartbeat() + }, 10000) + } + + const start = !this.active || JSON.stringify(this.config.server) != JSON.stringify(options.pool.server) + + let update_work = false + if(!start && this.active) { + if(this.config.mining.address != options.pool.mining.address || this.config.mining.uniform != options.pool.mining.uniform) { + update_work = true + } + } + + let update_vardiff = false + if(!start && this.active) { + if(JSON.stringify(this.config.varDiff) != JSON.stringify(options.pool.varDiff)) { + update_vardiff = true + } + } + + this.config = JSON.parse(JSON.stringify(options.pool)) + + if(update_work) { + this.getBlock(true).catch(() => {}) + } + if(update_vardiff) { + this.updateVarDiff() + } + + if(this.config.server.enabled) { + if(start && this.config.mining.address != "") { + this.start() + } + } else { + this.stop() + } + + } catch(error) { + logger.log("error", "Failed to start pool") + logger.log("error", error) + this.sendGateway("show_notification", {type: "negative", message: "Pool failed to start", timeout: 2000}) + } + } + + start() { + if(this.daemon_type == "remote") { + return false + } + this.stop().then(() => { + + logger.log("info", "Starting pool") + + this.sendStatus(1) + this.server = null + + this.connections = {} + this.blocks = { + current: null, + valid: [] + } + + if(this.intervals.startup) { + clearInterval(this.intervals.startup) + } + + let debounce = 0 + this.intervals.startup = setInterval(() => { + const daemon_info = this.backend.daemon.daemon_info + const target_height = Math.max(daemon_info.height_without_bootstrap, daemon_info.target_height) + + // if(daemon_info.is_ready && daemon_info.height_without_bootstrap >= target_height) { + if(daemon_info.height_without_bootstrap >= target_height) { + logger.log("info", "Attempting to connect to daemon") + + this.getBlock().then(() => { + clearInterval(this.intervals.startup) + this.startHeartbeat() + this.startServer().then(() => { + this.sendStatus(2) + }).catch(error => { + this.sendStatus(-1) + }) + }).catch(error => { + if(error == "Failed to parse wallet address") { + clearInterval(this.intervals.startup) + this.sendStatus(-1) + } + }) + + } else { + if(debounce++ % 30 == 0) { + logger.log("info", "Wait for daemon { is_ready: %s, height: %d, target_height: %d }", [daemon_info.is_ready, daemon_info.height_without_bootstrap, target_height]) + } + } + }, 2000) + + }) + } + + checkHeight() { + let url = "https://explorer.arqma.com/api/networkinfo" + if(this.testnet) { + url = "https://stageblocks.arqma.com/api/networkinfo" + } + return request(url) + } + + statsHeartbeat() { + this.sendGateway("set_pool_data", this.database.heartbeat()) + } + + startHeartbeat() { + if(this.intervals.timeout) { + clearInterval(this.intervals.timeout) + } + if(this.intervals.watchdog) { + clearInterval(this.intervals.watchdog) + } + + this.intervals.timeout = setInterval(() => { + for(let connection_id in this.connections) { + const miner = this.connections[connection_id] + if(Date.now() - miner.lastHeartbeat > this.config.mining.minerTimeout * 1000) { + logger.log("warn", "Worker timed out %s@%s", [miner.workerName, miner.ip]) + miner.socket.destroy() + delete this.connections[connection_id] + } + } + }, 30000) + + this.intervals.watchdog = setInterval(() => { + this.watchdog() + }, 240000) + this.watchdog() + + this.startJobRefreshInterval() + + this.startRetargetInterval() + } + + watchdog() { + // check for desynced daemon and incorrect local clock + this.checkHeight().then(response => { + try { + const json = JSON.parse(response) + if(json === null || typeof json !== "object" || !json.hasOwnProperty("data")) { + return + } + let desynced = false, system_clock_error = false + if(json.data.hasOwnProperty("height") && this.blocks.current != null) { + const remote_height = json.data.height + desynced = this.blocks.current.height < remote_height - 5 + if(desynced) { + logger.log("error", "Pool height is desynced { remote: %d, local: %d }", [remote_height, this.blocks.current.height]) + } else { + logger.log("info", "Pool height is okay { remote: %d, local: %d }", [remote_height, this.blocks.current.height]) + } + } + if(json.data.hasOwnProperty("server_time")) { + const allowed_time_variance = 15 * 60 // 15 minutes + const server_time = json.data.server_time + const system_time = Math.floor(Date.now() / 1000) + system_clock_error = Math.abs(server_time - system_time) > allowed_time_variance + if(system_clock_error) { + logger.log("error", "System clock is not correct { server: %d, local: %d }", [server_time, system_time]) + } else { + logger.log("info", "System clock is okay { server: %d, local: %d }", [server_time, system_time]) + } + } + this.sendGateway("set_pool_data", { desynced, system_clock_error }) + } catch(error) { + } + }).catch(() => { + }) + } + + startJobRefreshInterval() { + + let blockRefreshInterval = 1 * 1000 // 1 second + if(this.config.mining.enableBlockRefreshInterval) { + if(!Number.isNaN(this.config.mining.blockRefreshInterval * 1000)) { + blockRefreshInterval = this.config.mining.blockRefreshInterval * 1000 + } + } + + if(this.intervals.job) { + clearInterval(this.intervals.job) + } + this.intervals.job = setInterval(() => { + this.getBlock().catch(() => {}) + }, blockRefreshInterval) + } + + startRetargetInterval() { + if(this.intervals.retarget) { + clearInterval(this.intervals.retarget) + } + this.intervals.retarget = setInterval(() => { + for(let connection_id in this.connections) { + const miner = this.connections[connection_id] + if(miner.varDiff.enabled && miner.retarget()) { + logger.log("info", "Difficulty change { old: %d, new: %d } for %s@%s", [miner.difficulty.last, miner.difficulty.now, miner.workerName, miner.ip]) + } + } + }, this.config.varDiff.retargetTime * 1000) + } + + startServer() { + return new Promise((resolve, reject) => { + this.server = createServer(socket => { + let buffer = "" + socket.setKeepAlive(true) + socket.setEncoding("utf8") + socket.on("data", data => { + buffer += data + if(Buffer.byteLength(buffer, "utf8") > 10 * 10240) { + buffer = null + logger.log("warn", "Socket flooding from %s", [socket.remoteAddress]) + socket.destroy() + return + } + if(buffer.indexOf("\n") !== -1) { + let messages = buffer.split("\n") + if(buffer.endsWith("\n")) { + buffer = "" + } else { + buffer = messages.pop() + } + for(const message of messages) { + if(message.trim() == "") { + continue + } + let json = "" + try { + json = JSON.parse(message) + if(!json.id || !json.method || !json.params) { + logger.log("warn", "Invalid stratum call from %s", [socket.remoteAddress]) + return + } + } catch(error) { + logger.log("warn", "Malformed stratum call from %s", [socket.remoteAddress]) + socket.destroy() + break + } + try { + this.handleStratum(json, socket) + } catch(error) { + logger.log("error", "Error handling stratum call from %s", [socket.remoteAddress]) + logger.log("error", JSON.stringify(message)) + break + } + } + } + }).on("error", error => { + if(error.code !== "ECONNRESET") { + logger.log("warn", "Socket error from %s - %j", [socket.remoteAddress, error]) + } + }).on("close", () => { + logger.log("warn", "Socket closed from %s@%s", [socket.workerName, socket.remoteAddress]) + }) + }).listen(this.config.server.bindPort, this.config.server.bindIP, (error, result) => { + if(error) { + logger.log("error", "Could not start pool server on port %d", [this.config.server.bindPort, error]) + return reject() + } + logger.log("info", "Started server on port %d", [this.config.server.bindPort]) + resolve() + }).on("error", error => { + if(error.code == "EADDRINUSE") { + logger.log("warn", "Cannot bind on %s:%s - address in use", [this.config.server.bindIP, this.config.server.bindPort]) + reject() + } + }) + }) + } + + handleStratum(json, socket) { + + const reply = (error, result) => { + if(!socket.writable) { + return + } + const data = JSON.stringify({ + id: json.id, + jsonrpc: "2.0", + error: error ? {code: -1, message: error} : null, + result: result + }) + socket.write(data + "\n") + } + + const { method, params } = json + const miner = this.connections[params.id] + + if(method == "submit" || method == "keepalived") { + if(!miner) { + return reply("Unauthenticated") + } + } + + switch(method) { + + case "login": + const connection_id = uid() + const ip = socket.remoteAddress + const { login, pass, rigid } = params + + let workerName = "" + + if(rigid) { + workerName = rigid.trim() + } else if(pass) { + workerName = pass.trim() + if(workerName.toLowerCase() == "x") { + workerName = "" + } + } + if(workerName == "") { + workerName = "Unnamed_Worker" + } + workerName = workerName.normalize("NFD").replace(/\s+/g, "-").replace(/[^A-Za-z0-9\-\_]/gi, "") + + let { enabled, startDiff, minDiff, maxDiff, fixedDiffSeparator } = this.config.varDiff + let difficulty = startDiff + let fixed = false + const login_parts = login.split(fixedDiffSeparator) + + if(typeof enabled == "undefined") { + enabled = true + } + if(login_parts.length > 1) { + const login_diff = login_parts.pop() + if(/^\d+$/.test(login_diff)) { + fixed = true + enabled = false + difficulty = Math.max(Math.min(parseInt(login_diff), maxDiff), minDiff) + } + } + + const varDiff = { + ...this.config.varDiff, + difficulty, + enabled, + fixed + } + + const newMiner = new Miner(this, connection_id, workerName, varDiff, ip, socket) + + this.connections[connection_id] = newMiner + this.database.addWorker(workerName) + + socket.workerName = workerName + + logger.log("info", "Worker connected { difficulty: %d, fixed: %s } %s@%s", [difficulty, fixed, workerName, ip]) + + reply(null, { + id: connection_id, + job: newMiner.getJob(), + status: "OK" + }) + + break + + case "submit": + + const job_id = params.job_id + const hash = params.result + let nonce = params.nonce + let job = miner.findJob(job_id) + + if(!job) { + return reply("Invalid job id") + } + + if(!nonce || !noncePattern.test(nonce) || !hash) { + return reply("Invalid work") + } + + nonce = nonce.toLowerCase() + + if(!miner.checkJobSubmission(job, nonce)) { + return reply("Duplicate share") + } + + miner.addJobSubmission(job, nonce) + + const block = this.findBlock(job.height) + + if(!block) { + return reply("Block not found") + } + + this.processShare(job, block, nonce, hash).then(result => { + logger.log("info", "Accepted share { difficulty: %d, actual: %d } from %s@%s", [job.difficulty, result.diff, miner.workerName, miner.ip]) + reply(null, { status: "OK" }) + if(result.hash) { + logger.log("success", "Block found { hash: %s, height: %d } by %s@%s", [result.hash, job.height, miner.workerName, miner.ip]) + this.database.recordShare(miner, job, true, result.hash, block) + } else { + this.database.recordShare(miner, job, false) + } + miner.heartbeat() + miner.recordShare() + }).catch(error => { + logger.log("info", "Rejected share { difficulty: %d, actual: %d } from %s@%s", [job.difficulty, error.diff, miner.workerName, miner.ip]) + logger.log("error", "%s { height: %d } from worker %s@%s", [error.message, job.height, miner.workerName, miner.ip]) + reply("Invalid work") + }) + break + + case "keepalived": + miner.heartbeat() + reply(null, { status:"KEEPALIVED" }) + break + + default: + reply("Invalid method") + break + } + } + + processShare(job, block, nonce, hash) { + return new Promise((resolve, reject) => { + const hash_array = [...Buffer.from(hash, "hex")].reverse() + const hash_bigint = BigInt.fromArray(hash_array, 256, false) + const hash_diff = diff1.divide(hash_bigint) + if(hash_diff.geq(block.difficulty)) { + logger.log("info", "Block candidate { height: %d, diff: %d, target: %d, nonce: %s }", [job.height, hash_diff, block.difficulty, nonce]) + let block_blob = "" + try { + let template = Buffer.from(block.buffer) + block.writeExtraNonce(job.extra_nonce, template) + + logger.log("warn", "contruct_block_blob input") + logger.log("warn", template.toString("hex")) + + block_blob = this.core_bridge.construct_block_blob(template.toString("hex"), Buffer.from(nonce, "hex").readUInt32LE(0)) + } catch(error) { + return reject({ message: "Error constructing block blob", diff: hash_diff }) + } + this.submitBlock(block_blob).then(data => { + if(data.hasOwnProperty("error")) { + return reject({ message: "Error submitting block", diff: hash_diff }) + } + let block_fast_hash = "0000000000000000000000000000000000000000000000000000000000000000" + try { + block_fast_hash = this.core_bridge.get_block_id(block_blob) + } catch(error) { + logger.log("warn", "Get block id failed") + } + this.getBlock().catch(() => {}) + return resolve({ hash: block_fast_hash, diff: hash_diff }) + }) + } else if(hash_diff.lt(job.difficulty)) { + return reject({ message: "Rejected low difficulty share", diff: hash_diff }) + } else { + return resolve({ hash: false, diff: hash_diff }) + } + }) + } + + updateVarDiff() { + for(let connection_id in this.connections) { + const miner = this.connections[connection_id] + if(!miner.varDiff.fixed) { + miner.updateVarDiff({ + ...miner.varDiff, + ...this.config.varDiff + }) + miner.pushJob(true) + } + } + this.startRetargetInterval() + } + + findBlock(height) { + return this.blocks.valid.filter(block => block.height == height).pop() + } + + getBlock(force=false) { + const wallet_address= this.config.mining.address + const uniform = this.config.mining.uniform || Object.keys(this.connections).length > 128 + const reserve_size = uniform ? 8 : 1 + return new Promise((resolve, reject) => { + this.sendRPC("get_block_template", { wallet_address, reserve_size }).then(data => { + if(data.hasOwnProperty("error")) { + logger.log("error", "Error polling get_block_template %j", [data.error.message]) + return reject(data.error.message) + } + const block = data.result + if(this.blocks.current == null || this.blocks.current.height < block.height || force) { + const address_abbr = wallet_address.substring(0, 5) + "..." + wallet_address.substring(wallet_address.length - 5) + logger.log("info", "New block to mine { address: %s, height: %d, difficulty: %d, uniform: %s }", [address_abbr, block.height, block.difficulty, uniform]) + + this.blocks.current = new Block(this, block, uniform) + + this.blocks.valid.push(this.blocks.current) + + while(this.blocks.valid.length > 5) { + this.blocks.valid.shift() + } + + for(let connection_id in this.connections) { + const miner = this.connections[connection_id] + miner.pushJob(force) + } + } + resolve() + }) + }) + } + + submitBlock(block) { + return this.sendRPC("submit_block", [block], false) + } + + sendRPC(method, params={}, queue=true) { + if(queue) { + return this.backend.daemon.sendRPC(method, params) + } + let id = this.id++ + let options = { + uri: `${this.protocol}${this.hostname}:${this.port}/json_rpc`, + method: "POST", + json: { + jsonrpc: "2.0", + id: id, + method: method + }, + agent: this.agent + } + if(Array.isArray(params) || Object.keys(params).length !== 0) { + options.json.params = params + } + return request(options).then(response => { + if(response.hasOwnProperty("error")) { + return { + method: method, + params: params, + error: response.error + } + } + return { + method: method, + params: params, + result: response.result + } + }).catch(error => { + return { + method: method, + params: params, + error: { + code: -1, + message: "Cannot connect to daemon-rpc", + cause: error.cause + } + } + }) + } + + sendStatus(status) { + // -1: error, 0: disabled, 1: waiting, 2: enabled + this.active = status == 2 + this.sendGateway("set_pool_data", { status }) + } + sendGateway(method, data) { + this.backend.send(method, data) + } + + stop() { + return new Promise((resolve, reject) => { + this.sendStatus(0) + + if(this.intervals.startup) { + clearInterval(this.intervals.startup) + } + if(this.intervals.job) { + clearInterval(this.intervals.job) + } + if(this.intervals.timeout) { + clearInterval(this.intervals.timeout) + } + if(this.intervals.watchdog) { + clearInterval(this.intervals.watchdog) + } + if(this.intervals.retarget) { + clearInterval(this.intervals.retarget) + } + for(let connection_id in this.connections) { + const miner = this.connections[connection_id] + logger.log("warn", "Closing connection %s@%s", [miner.workerName, miner.ip]) + miner.socket.destroy() + delete this.connections[connection_id] + } + if(this.server) { + logger.log("warn", "Closing server") + this.server.close(() => { + logger.log("warn", "Server closed") + resolve() + }) + } else { + resolve() + } + }) + } + + quit() { + return new Promise((resolve, reject) => { + this.stop().then(() => { + if(this.intervals.stats) { + clearInterval(this.intervals.stats) + } + if(this.agent) { + this.agent.destroy() + this.agent = null + } + if(this.database) { + logger.log("warn", "Stopping database") + this.database.stop() + resolve() + } + }) + }) + } +} diff --git a/src-electron/main-process/modules/pool/block.js b/src-electron/main-process/modules/pool/block.js new file mode 100644 index 0000000..e807f61 --- /dev/null +++ b/src-electron/main-process/modules/pool/block.js @@ -0,0 +1,55 @@ +import { randomBytes } from "crypto" + +export class Block { + constructor(pool, template, uniform=true) { + + this.pool = pool + this.template = template + this.uniform = uniform + this.seed_hash = template.seed_hash + this.next_seed_hash = template.next_seed_hash + this.blockhashing_blob = template.blocktemplate_blob + this.extra_nonce = 0 + this.height = template.height + this.difficulty = template.difficulty + this.address = pool.config.mining.address + + this.buffer = Buffer.from(template.blocktemplate_blob, "hex") + + if(uniform) { + /* Uniform mode + * when enabled, we will mimic normal pool + * set extra_nonce to random number between 0-31 + * also set "instanceID" to random four bytes + */ + this.extra_nonce = randomBytes(1).readUInt8() % 32 + randomBytes(4).copy(this.buffer, template.reserved_offset + 4) + } + } + newBlob() { + this.extra_nonce++ + if(!this.uniform) { + this.extra_nonce = this.extra_nonce % 256 + } + this.writeExtraNonce(this.extra_nonce) + return this.convertBlob() + } + convertBlob() { + try { + return this.pool.core_bridge.convert_blob(this.buffer.toString("hex")) + } catch(e) { + return false + } + } + writeExtraNonce(extra_nonce, buffer=false) { + if(!buffer) { + buffer = this.buffer + } + if(this.uniform) { + buffer.writeUInt32BE(extra_nonce, this.template.reserved_offset) + } else { + buffer.writeUInt8(extra_nonce % 256, this.template.reserved_offset) + } + return buffer + } +} diff --git a/src-electron/main-process/modules/pool/database.js b/src-electron/main-process/modules/pool/database.js new file mode 100644 index 0000000..dd6482d --- /dev/null +++ b/src-electron/main-process/modules/pool/database.js @@ -0,0 +1,350 @@ +import SQL from "better-sqlite3" +import { join } from "path" +import { logger } from "./utils" + +//const path = require("path") +//const SQL = require("better-sqlite3") + +export class Database { + constructor(pool, options) { + this.pool = pool + this.db = null + this.stats = {} + if(options.testnet) { + this.sqlitePath = join(options.data_dir, "gui", "pool_stats_testnet.sqlite") + } else { + this.sqlitePath = join(options.data_dir, "gui", "pool_stats.sqlite") + } + this.vacuum_interval = 1000 * 60 * 60 * 24 // 24 hours + } + + start() { + + this.db = new SQL(this.sqlitePath) + + this.getTables() + // tables: [ { name: 'round' }, { name: 'hashrate' }, { name: 'hashrateAvg' }, { name: 'workers' }, { name: 'blocks' } ] + // Later version may check for existence of tables and run upgrade procedue instead + + this.init() + + this.stmt = { + + blocks: this.db.prepare("SELECT * FROM blocks ORDER BY height desc"), + blocks_status_0: this.db.prepare("SELECT * FROM blocks WHERE status = 0"), + blocks_update: this.db.prepare("UPDATE blocks SET status = :status, reward = :reward WHERE hash = :hash"), + blocks_add: this.db.prepare("INSERT INTO blocks(hash, height, reward, miner, timeFound, minedTo, diff, hashes, status) VALUES(:hash, :height, :reward, :miner, :timeFound, :minedTo, :diff, :hashes, :status)"), + + workers: this.db.prepare("SELECT * FROM workers"), + worker_add: this.db.prepare("INSERT OR IGNORE INTO workers(miner, lastShare, hashes) VALUES(:miner, :lastShare, 0)"), + worker_update: this.db.prepare("UPDATE workers SET hashes = hashes + :hashes, lastShare = :lastShare WHERE miner = :miner"), + workers_clean: this.db.prepare("DELETE FROM workers WHERE lastShare < :time"), + + round_hashes: this.db.prepare("SELECT SUM(hashes) as hashes FROM round"), + round_add_worker: this.db.prepare("INSERT OR IGNORE INTO round(miner, hashes) VALUES(:miner, 0)"), + round_update_worker: this.db.prepare("UPDATE round SET hashes = hashes + :hashes WHERE miner = :miner"), + round_clear: this.db.prepare("DELETE FROM round"), + + hashrate_add: this.db.prepare("INSERT INTO hashrate(miner, time, hashes) VALUES(:miner, :time, :hashes)"), + hashrate_calc: this.db.prepare("SELECT miner, SUM(hashes) as hashes, MIN(time) as start_time, MAX(time) as end_time FROM hashrate WHERE time BETWEEN :start_time AND :end_time GROUP BY miner"), + hashrate_clean: this.db.prepare("DELETE FROM hashrate WHERE time < :time"), + + hashrate_avg: this.db.prepare("SELECT * FROM hashrateAvg"), + hashrate_avg_add: this.db.prepare("INSERT OR IGNORE INTO hashrateAvg(miner, time, hashes) VALUES(:miner, :time, :hashes)"), + hashrate_avg_clean: this.db.prepare("DELETE FROM hashrate WHERE time < :time"), + } + + + this.vacuum() + + setInterval(() => { + this.vacuum() + }, this.vacuum_interval) + + } + + stop() { + if(this.db) { + this.db.close() + } + } + + getTables() { + return this.db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all() + } + + vacuum() { + if(!this.db) { + return + } + try { + this.db.exec("VACUUM") + logger.log("info", "Success vacuuming database") + } catch(error) { + logger.log("error", "Error vacuuming database") + } + } + + init() { + this.db.prepare("CREATE TABLE IF NOT EXISTS round(miner TEXT PRIMARY KEY, hashes INTEGER) WITHOUT ROWID;").run() + this.db.prepare("CREATE TABLE IF NOT EXISTS hashrate(miner TEXT, time DATETIME, hashes INTEGER);").run() + this.db.prepare("CREATE TABLE IF NOT EXISTS hashrateAvg(miner TEXT, time DATETIME, hashes INTEGER);").run() + this.db.prepare("CREATE TABLE IF NOT EXISTS workers(miner TEXT PRIMARY KEY, lastShare DATETIME, hashes INTEGER) WITHOUT ROWID;").run() + this.db.prepare("CREATE TABLE IF NOT EXISTS blocks(hash TEXT PRIMARY KEY, height INTEGER, reward INTEGER, miner TEXT, timeFound DATETIME, minedTo TEXT, diff INTEGER, hashes INTEGER, status INTEGER) WITHOUT ROWID;").run() + } + + heartbeat() { + const dateNow = Date.now() + + this.unlockBlocks() + let blocks = this.getBlocks() + let workers = { + "__global": {}, + ...this.getWorkers() + } + + let activeWorkers = 0 + for(let worker of Object.keys(workers)) { + if(workers[worker].hasOwnProperty("lastShare") && workers[worker].lastShare > dateNow - 10 * 60 * 1000) { // 10 minutes + workers[worker].active = true + activeWorkers++ + } else { + workers[worker].active = false + } + } + + const hashrates = this.getHashrates(workers) + let h = { + hashrate_5min: 0, + hashrate_1hr: 0, + hashrate_6hr: 0, + hashrate_24hr: 0, + } + + for(let worker of Object.keys(hashrates)) { + workers[worker].hashrate_graph = hashrates[worker].hashrate_graph + h.hashrate_5min += workers[worker].hashrate_5min = hashrates[worker].hashrate_5min + h.hashrate_1hr += workers[worker].hashrate_1hr = hashrates[worker].hashrate_1hr + h.hashrate_6hr += workers[worker].hashrate_6hr = hashrates[worker].hashrate_6hr + h.hashrate_24hr += workers[worker].hashrate_24hr = hashrates[worker].hashrate_24hr + } + + this.cleanStats() + + const blockHashes = Object.keys(blocks) + + let diff = 0 + let height = 0 + if(this.pool.blocks && this.pool.blocks.current) { + diff = this.pool.blocks.current.difficulty + height = this.pool.blocks.current.height + } + + let averageEffort = 0 + if(blockHashes.length) { + for(let hash of blockHashes) { + let block = blocks[hash] + averageEffort += block.hashes / block.diff + } + averageEffort /= blockHashes.length + } + + const roundHashes = this.getRoundHashes() + let effort = 0 + if(diff != 0) { + effort = Math.round(100 * roundHashes / diff) / 100 + } + + const blocksFound = Object.keys(blocks).length + const networkHashrate = diff / 120 + const blockTime = 1000 * 120 * networkHashrate / h.hashrate_5min + + this.stats = { + stats: { + h, + diff, + activeWorkers, + roundHashes, + effort, + averageEffort, + blockTime, + blocksFound, + networkHashrate, + height + }, + blocks: Object.values(blocks), + workers: Object.values(workers) + } + + return this.stats + + } + + unlockBlocks() { + const blocks = this.stmt.blocks_status_0.all() + for(const block of blocks) { + this.pool.sendRPC("get_block", { height: block.height }).then(data => { + if(data.hasOwnProperty("error")) { + logger.log("error", "Error calling get_block %j", [data.error.message]) + return false + } + if(block.reward == -1) { + const json = JSON.parse(data.result.json) + const reward = json.miner_tx.vout[0].amount + this.stmt.blocks_update.run({ status: 0, reward: reward, hash: block.hash }) + } + if(data.result.block_header.hash != block.hash) { + logger.log("error", "Block %s ophaned", [block.hash]) + this.stmt.blocks_update.run({ status: 1, reward: 0, hash: block.hash }) + } + if(data.result.block_header.depth > 18) { + logger.log("success", "Block %s unlocked", [block.hash]) + this.stmt.blocks_update.run({ status: 2, reward: block.reward, hash: block.hash }) + } + }) + } + } + + getBlocks() { + let blocks = {} + for(const block of this.stmt.blocks.all()) { + blocks[block.hash] = block + } + return blocks + } + + getWorkers() { + let workers = {} + for(const worker of this.stmt.workers.all()) { + workers[worker.miner] = worker + } + return workers + } + + cleanStats() { + const one_day = Date.now() - 24 * 60 * 60 * 1000 + this.stmt.hashrate_clean.run({ time: one_day }) + this.stmt.hashrate_avg_clean.run({ time: one_day }) + this.stmt.workers_clean.run({ time: one_day }) + } + + getHashrates(workers) { + const dateNow = Date.now() + const start_of_two_minute = dateNow - (dateNow % (2 * 60 * 1000)) + + let hashrates = {} + for(let worker of Object.keys(workers)) { + hashrates[worker] = { + hashrate_5min: 0, + hashrate_1hr: 0, + hashrate_6hr: 0, + hashrate_24hr: 0, + hashrate_graph: {} + } + } + + const hashrate_5min = this.calcHashrate(5*60) + const hashrate_1hr = this.calcHashrate(60*60) + const hashrate_6hr = this.calcHashrate(6*60*60) + const hashrate_24hr = this.calcHashrate(24*60*60) + const hashrate_graph = this.getHashrateGraph(workers) + + for(let worker of Object.keys(hashrate_5min)) { + this.stmt.hashrate_avg_add.run({ miner: worker, time: start_of_two_minute, hashes: hashrate_5min[worker] }) + hashrates[worker].hashrate_5min = hashrate_5min[worker] + } + for(let worker of Object.keys(hashrate_1hr)) { + hashrates[worker].hashrate_1hr = hashrate_1hr[worker] + } + for(let worker of Object.keys(hashrate_6hr)) { + hashrates[worker].hashrate_6hr = hashrate_6hr[worker] + } + for(let worker of Object.keys(hashrate_24hr)) { + hashrates[worker].hashrate_24hr = hashrate_24hr[worker] + } + for(let worker of Object.keys(hashrate_graph)) { + hashrates[worker].hashrate_graph = hashrate_graph[worker] + } + + return hashrates + } + + calcHashrate(n_time = 300, end_time = false) { + if(!end_time) { + end_time = Date.now() + } + const start_time = end_time - n_time * 1000 + + let hashrates = {} + for(const h of this.stmt.hashrate_calc.all({ start_time, end_time })) { + hashrates[h.miner] = Math.round(100 * h.hashes / Math.max(300, (end_time - h.start_time) / 1000) ) / 100 + } + return hashrates + } + + getHashrateGraph(workers) { + const dateNow = Date.now() + const start_of_two_minute = dateNow - (dateNow % (2 * 60 * 1000)) + + let graphs = {} + for(let worker of Object.keys(workers)) { + graphs[worker] = {} + // initialize hashrate graph to empty + for(let j = start_of_two_minute - 24 * 60 * 60 * 1000; j <= start_of_two_minute; j += 2 * 60 * 1000) { + graphs[worker][j] = 0 + } + } + + for(const h of this.stmt.hashrate_avg.all()) { + if(graphs.hasOwnProperty(h.miner)) { + graphs[h.miner][h.time] = h.hashes + } + } + + return graphs + } + + getRoundHashes() { + const row = this.stmt.round_hashes.get() + return typeof row !== "undefined" ? row.hashes : 0 + } + + addWorker(workerName) { + this.stmt.worker_add.run({ miner: workerName, lastShare: Date.now() }) + } + + recordShare(miner, job, blockCandidate, hash, blockTemplate) { + const dateNow = Date.now() + const workerName = miner.workerName + const shareDiff = job.difficulty + + // Create rows for this miner if not yet exist + this.stmt.round_add_worker.run({ miner: workerName }) + this.stmt.worker_add.run({ miner: workerName, lastShare: dateNow }) + + // Insert stats for current round and overall miner stats + this.stmt.round_update_worker.run({ hashes: shareDiff, miner: workerName }) + this.stmt.worker_update.run({ hashes: shareDiff, lastShare: dateNow, miner: workerName }) + + // Add hashrate record + this.stmt.hashrate_add.run({ miner: workerName, time: dateNow, hashes: shareDiff }) + + // If is block, add to block table + if(blockCandidate) { + const totalHashes = this.getRoundHashes() + this.stmt.round_clear.run() + this.stmt.blocks_add.run({ + hash: hash, + height: job.height, + reward: -1, + miner: workerName, + timeFound: dateNow, + minedTo: blockTemplate.address, + diff: blockTemplate.difficulty, + hashes: totalHashes, + status: 0 + }) + } + + } +} diff --git a/src-electron/main-process/modules/pool/miner.js b/src-electron/main-process/modules/pool/miner.js new file mode 100644 index 0000000..3c09c64 --- /dev/null +++ b/src-electron/main-process/modules/pool/miner.js @@ -0,0 +1,187 @@ +import { diff1, uid, logger } from "./utils" + +export class Miner { + constructor(pool, id, workerName, varDiff, ip, socket) { + this.pool = pool + this.id = id + this.workerName = workerName + this.ip = ip + this.socket = socket + + this.varDiff = varDiff + this.difficulty = { + now: varDiff.difficulty, + pending: null, + last: null + } + + this.lastHeight = null + + this.jobs = [] + this.shareTimes = [] + + this.lastShare = Date.now() / 1000 + this.heartbeat() + } + + push(method, params) { + if(!this.socket.writable) { + return + } + this.socket.write(JSON.stringify({ jsonrpc: "2.0", method, params }) + "\n") + } + + heartbeat() { + this.lastHeartbeat = Date.now() + } + + retarget() { + const { targetTime, maxDiff, minDiff, variancePercent, maxJump} = this.varDiff + const variance = targetTime * variancePercent / 100 + const targetMin = targetTime - variance + const targetMax = targetTime + variance + + const dateNow = Date.now() / 1000 + const shareDelta = dateNow - this.lastShare + const shareExtra = shareDelta >= targetMax ? shareDelta : 0 + const shareAvg = this.calcShareAvg(shareExtra) + + logger.log("info", "Share time stats { target: %ds, average: %ds, since_last: %ds, buffer: %d } for %s@%s", [targetTime, Math.round(shareAvg || 0), Math.round(shareDelta), this.shareTimes.length, this.workerName, this.ip]) + + if(targetMin <= shareAvg && shareAvg <= targetMax) { + return false + } + if(shareAvg < targetMin && this.shareTimes.length < 6) { + return false + } + if(shareAvg > targetMax && shareDelta < 2 * targetTime) { + return false + } + + if(shareDelta < targetMax) { + if(shareAvg == 0 || this.shareTimes.length == 0) { + return false + } + } else { + this.lastShare = dateNow + } + + this.shareTimes = [] + + const newDiffMin = this.difficulty.now * (1 - maxJump / 100) + const newDiffMax = this.difficulty.now * (1 + maxJump / 100) + + let newDiff + newDiff = this.difficulty.now * targetTime / shareAvg + newDiff = Math.min(newDiff, newDiffMax, maxDiff) + newDiff = Math.max(newDiff, newDiffMin, minDiff) + newDiff = Math.round(newDiff) + + if(!isNaN(newDiff) && this.difficulty.now != newDiff) { + this.difficulty.pending = newDiff + this.pushJob() + return true + } + return false + } + + updateVarDiff(varDiff) { + this.varDiff = varDiff + this.lastShare = Date.now() / 1000 + this.shareTimes = [] + this.difficulty = { + now: varDiff.startDiff, + pending: null, + last: null + } + } + + recordShare() { + const dateNow = Date.now() / 1000 + this.shareTimes.push(dateNow - this.lastShare) + while(this.shareTimes.length > 16) { + this.shareTimes.shift() + } + this.lastShare = dateNow + } + + calcShareAvg(extra=0) { + return this.shareTimes.reduce((sum, x) => sum + x, extra) / (this.shareTimes.length + (extra ? 1 : 0)) + } + + addJobSubmission(job, nonce) { + job.submissions.push(nonce) + } + + checkJobSubmission(job, nonce) { + return job.submissions.indexOf(nonce) == -1 + } + + pushJob(force=false) { + this.push("job", this.getJob(force)) + } + + addJob(job) { + this.jobs.push(job) + } + + findJob(job_id) { + return this.jobs.filter(job => job.id == job_id).pop() + } + + getJob(force=false) { + const block = this.pool.blocks.current + let job_id = "", blob = "", target = "", seed_hash = "", next_seed_hash = "" + if(block.height != this.lastHeight || this.difficulty.pending || force) { + this.lastHeight = block.height + if(this.difficulty.pending) { + this.difficulty = { + now: this.difficulty.pending, + pending: null, + last: this.difficulty.now + } + } + + let difficulty = Math.min(this.difficulty.now, block.difficulty-1) + + job_id = uid() + blob = block.newBlob() + target = this.targetToCompact(difficulty) + seed_hash = block.seed_hash + next_seed_hash = block.next_seed_hash + this.addJob({ + id: job_id, + extra_nonce: block.extra_nonce, + height: block.height, + difficulty: difficulty, + submissions: [], + seed_hash: block.seed_hash, + next_seed_hash: block.next_seed_hash + }) + while(this.jobs.length > 4) { + this.jobs.shift() + } + } + return { blob, job_id, target, seed_hash, next_seed_hash } + } + + targetToCompact(diff) { + let padded = Buffer.alloc(32) + padded.fill(0) + + const diffArray = diff1.divide(diff).toArray(256).value + let diffBuff = Buffer.from(diffArray) + + diffBuff.copy(padded, 32 - diffBuff.length) + + const buff = padded.slice(0, 4) + let buffReversed = Buffer.allocUnsafe(buff.length) + + for(let i = 0, j = buff.length - 1; i <= j; ++i, --j) { + buffReversed[i] = buff[j] + buffReversed[j] = buff[i] + } + + return buffReversed.toString("hex") + } +} diff --git a/src-electron/main-process/modules/pool/utils.js b/src-electron/main-process/modules/pool/utils.js new file mode 100644 index 0000000..9b90188 --- /dev/null +++ b/src-electron/main-process/modules/pool/utils.js @@ -0,0 +1,70 @@ +import BigInt from "big-integer" +import dateFormat from "dateformat" +import createWriteStream from "rotating-file-stream" +// import { createWriteStream } from "fs" +import { randomBytes } from "crypto" +import { format } from "util" + +export const noncePattern = new RegExp("^[0-9A-Fa-f]{8}$") + +export const diff1 = BigInt("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", 16) + +const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" +export function uid(length=15) { + const bytes = randomBytes(length) + let r = [] + for(let i = 0; i < bytes.length; i++) { + r.push(alphabet[bytes[i] % alphabet.length]) + } + return r.join("") +} + +class Logger { + + constructor() { + this.stream = null + } + + setLogFile(path, file) { + // this.stream = createWriteStream(join(path, file), { flags: "a" }) + this.stream = createWriteStream(file, { + path, + size: "5M", + interval: "1d", + maxFiles: 10, + compress: "gzip" + }) + } + + log(level, message, params=[]) { + const timestamp = dateFormat(new Date(), "yyyy-mm-dd HH:MM:ss.l") + message = format(message, ...params) + message = `${timestamp} [POOL] ${message}` + + if(this.stream) { + this.stream.write(message+"\n") + } + + const color_reset = "\x1b[0m" + let color = color_reset + switch(level) { + case "error": + color = "\x1b[31m" + break + case "success": + color = "\x1b[32m" + break + case "warn": + color = "\x1b[33m" + break + case "info": + color = "\x1b[34m" + break + } + + console.log(color+message+color_reset) + } + +} + +export let logger = new Logger() diff --git a/src-electron/main-process/modules/status-codes.js b/src-electron/main-process/modules/status-codes.js deleted file mode 100644 index d7488cd..0000000 --- a/src-electron/main-process/modules/status-codes.js +++ /dev/null @@ -1,3 +0,0 @@ -export const WALLET_NOT_OPEN = -1 -export const WALLET_OPEN = 0 -export const WALLET_ERROR = 1 diff --git a/src-electron/main-process/modules/wallet-rpc.js b/src-electron/main-process/modules/wallet-rpc.js index 0cf813d..a4f88ed 100644 --- a/src-electron/main-process/modules/wallet-rpc.js +++ b/src-electron/main-process/modules/wallet-rpc.js @@ -1,21 +1,20 @@ -import child_process from "child_process" -const request = require("request-promise") -const queue = require("promise-queue") -const http = require("http") -const os = require("os") -const fs = require("fs-extra") -const path = require("path") -const crypto = require("crypto") -const portscanner = require("portscanner") +import child_process from "child_process"; +const request = require("request-promise"); +const queue = require("promise-queue"); +const http = require("http"); +const os = require("os"); +const fs = require("fs"); +const path = require("path"); +const crypto = require("crypto"); export class WalletRPC { - constructor (backend) { + constructor(backend) { this.backend = backend this.data_dir = null this.wallet_dir = null this.auth = [] this.id = 0 - this.net_type = "mainnet" + this.testnet = false this.heartbeat = null this.wallet_state = { open: false, @@ -24,347 +23,281 @@ export class WalletRPC { balance: null, unlocked_balance: null } - this.isRPCSyncing = false - this.dirs = null + this.wallet_info = { + height: 0 + } + + this.wallet_list = [] + this.last_height_send_time = Date.now() - this.height_regexes = [ - { - string: /Processed block: <([a-f0-9]+)>, height (\d+)/, - height: (match) => match[2] - }, - { - string: /Skipped block by height: (\d+)/, - height: (match) => match[1] - }, - { - string: /Skipped block by timestamp, height: (\d+)/, - height: (match) => match[1] - }, - { - string: /Blockchain sync progress: <([a-f0-9]+)>, height (\d+)/, - height: (match) => match[2] - } - ] + this.height_regex1 = /Processed block: <([a-f0-9]+)>, height (\d+)/ + this.height_regex2 = /Skipped block by height: (\d+)/ + this.height_regex3 = /Skipped block by timestamp, height: (\d+)/ - this.agent = new http.Agent({ keepAlive: true, maxSockets: 1 }) + this.agent = new http.Agent({keepAlive: true, maxSockets: 1}) this.queue = new queue(1, Infinity) + } // this function will take an options object for testnet, data-dir, etc - start (options) { - const { net_type } = options.app - const daemon = options.daemons[net_type] + start(options) { return new Promise((resolve, reject) => { - let daemon_address = `${daemon.rpc_bind_ip}:${daemon.rpc_bind_port}` - if (daemon.type == "remote") { - daemon_address = `${daemon.remote_host}:${daemon.remote_port}` + + let daemon_address = `${options.daemon.rpc_bind_ip}:${options.daemon.rpc_bind_port}` + if(options.daemon.type == "remote") { + daemon_address = `${options.daemon.remote_host}:${options.daemon.remote_port}` } - crypto.randomBytes(64 + 64 + 32, (err, buffer) => { - if (err) throw err + crypto.randomBytes(64+64+32, (err, buffer) => { + if(err) throw err let auth = buffer.toString("hex") this.auth = [ - auth.substr(0, 64), // rpc username - auth.substr(64, 64), // rpc password - auth.substr(128, 32) // password salt + auth.substr(0,64), // rpc username + auth.substr(64,64), // rpc password + auth.substr(128,32), // password salt ] const args = [ - "--rpc-login", this.auth[0] + ":" + this.auth[1], + "--rpc-login", this.auth[0]+":"+this.auth[1], "--rpc-bind-port", options.wallet.rpc_bind_port, "--daemon-address", daemon_address, - // "--log-level", options.wallet.log_level, - "--log-level", "*:WARNING,net*:FATAL,net.http:DEBUG,global:INFO,verify:FATAL,stacktrace:INFO" + //"--log-level", options.wallet.log_level, + "--log-level", "*:WARNING,net*:FATAL,net.http:DEBUG,global:INFO,verify:FATAL,stacktrace:INFO", ] - const { net_type, wallet_data_dir, data_dir } = options.app - this.net_type = net_type - this.data_dir = data_dir - this.wallet_data_dir = wallet_data_dir + let log_file - this.dirs = { - "mainnet": this.wallet_data_dir, - "stagenet": path.join(this.wallet_data_dir, "stagenet"), - "testnet": path.join(this.wallet_data_dir, "testnet") - } + this.data_dir = options.app.data_dir - this.wallet_dir = path.join(this.dirs[net_type], "wallets") - args.push("--wallet-dir", this.wallet_dir) - - const log_file = path.join(this.dirs[net_type], "logs", "wallet-rpc.log") - args.push("--log-file", log_file) - - if (net_type === "testnet") { + if(options.app.testnet) { + this.testnet = true + this.wallet_dir = path.join(options.app.data_dir, "testnet", "wallets") + log_file = path.join(options.app.data_dir, "testnet", "logs", "wallet-rpc.log") args.push("--testnet") - } else if (net_type === "stagenet") { - args.push("--stagenet") + args.push("--log-file", log_file) + args.push("--wallet-dir", this.wallet_dir) + } else { + this.wallet_dir = path.join(options.app.data_dir, "wallets") + log_file = path.join(options.app.data_dir, "logs", "wallet-rpc.log") + args.push("--log-file", log_file) + args.push("--wallet-dir", this.wallet_dir) } - if (fs.existsSync(log_file)) { fs.truncateSync(log_file, 0) } + if (fs.existsSync(log_file)) + fs.truncateSync(log_file, 0) - if (!fs.existsSync(this.wallet_dir)) { fs.mkdirpSync(this.wallet_dir) } + if (process.platform === "win32") { + this.walletRPCProcess = child_process.spawn(path.join(__arqma_bin, "arqma-wallet-rpc.exe"), args) + } else { + this.walletRPCProcess = child_process.spawn(path.join(__arqma_bin, "arqma-wallet-rpc"), args, { + detached: true + }) + } // save this info for later RPC calls this.protocol = "http://" this.hostname = "127.0.0.1" this.port = options.wallet.rpc_bind_port - portscanner.checkPortStatus(this.port, this.hostname).catch(e => "closed").then(status => { - if (status === "closed") { - if (process.platform === "win32") { - // eslint-disable-next-line no-undef - this.walletRPCProcess = child_process.spawn(path.join(__arqma_bin, "arqma-wallet-rpc.exe"), args) - } else { - // eslint-disable-next-line no-undef - this.walletRPCProcess = child_process.spawn(path.join(__arqma_bin, "arqma-wallet-rpc"), args, { - detached: true - }) - } + this.walletRPCProcess.stdout.on("data", (data) => { - this.walletRPCProcess.stdout.on("data", (data) => { - process.stdout.write(`Wallet: ${data}`) - - let lines = data.toString().split("\n") - let match, height = null - let isRPCSyncing = false - for (const line of lines) { - for (const regex of this.height_regexes) { - match = line.match(regex.string) - if (match) { - height = regex.height(match) - isRPCSyncing = true - break - } - } - } + process.stdout.write(`Wallet: ${data}`) - // Keep track on wether a wallet is syncing or not - this.sendGateway("set_wallet_data", { isRPCSyncing }) - this.isRPCSyncing = isRPCSyncing - - if (height && Date.now() - this.last_height_send_time > 1000) { - this.last_height_send_time = Date.now() - this.sendGateway("set_wallet_data", { - info: { - height - } - }) + let lines = data.toString().split("\n"); + let match, height = null + lines.forEach((line) => { + match = line.match(this.height_regex1) + if (match) { + height = match[2] + } else { + match = line.match(this.height_regex2) + if (match) { + height = match[1] + } else { + match = line.match(this.height_regex3) + if (match) { + height = match[1] + } } - }) - this.walletRPCProcess.on("error", err => process.stderr.write(`Wallet: ${err}`)) - this.walletRPCProcess.on("close", code => { - process.stderr.write(`Wallet: exited with code ${code} \n`) - this.walletRPCProcess = null - this.agent.destroy() - if (code === null) { - reject(new Error("Failed to start wallet RPC")) + } + }) + if(height && Date.now() - this.last_height_send_time > 1000) { + this.last_height_send_time = Date.now() + this.sendGateway("set_wallet_data", { + info: { + height } }) - - // To let caller know when the wallet is ready - let intrvl = setInterval(() => { - this.sendRPC("get_languages").then((data) => { - if (!data.hasOwnProperty("error")) { - clearInterval(intrvl) - resolve() - } else { - if (data.error.cause && - data.error.cause.code === "ECONNREFUSED") { - // Ignore - } else { - clearInterval(intrvl) - if (this.walletRPCProcess) this.walletRPCProcess.kill() - this.walletRPCProcess = null - reject(new Error("Could not connect to wallet RPC")) - } - } - }) - }, 1000) - } else { - reject(new Error(`Wallet RPC port ${this.port} is in use`)) } }) + this.walletRPCProcess.on("error", err => process.stderr.write(`Wallet: ${err}\n`)) + this.walletRPCProcess.on("close", code => process.stderr.write(`Wallet: exited with code ${code}\n`)) + + // To let caller know when the wallet is ready + let intrvl = setInterval(() => { + this.sendRPC("get_languages").then((data) => { + if(!data.hasOwnProperty("error")) { + clearInterval(intrvl) + resolve() + } else { + if(data.error.cause && + data.error.cause.code === "ECONNREFUSED") { + // Ignore + } else { + clearInterval(intrvl) + reject(data.error) + } + } + }) + }, 1000) }) }) } - handle (data) { + handle(data) { + let params = data.data switch (data.method) { - case "has_password": - this.hasPassword() - break - - case "validate_address": - this.validateAddress(params.address) - break - - case "copy_old_gui_wallets": - this.copyOldGuiWallets(params.wallets || []) - break - - case "list_wallets": - this.listWallets() - break - - case "create_wallet": - this.createWallet(params.name, params.password, params.language) - break - - case "restore_wallet": - this.restoreWallet(params.name, params.password, params.seed, - params.refresh_type, params.refresh_type == "date" ? params.refresh_start_date : params.refresh_start_height) - break - - case "restore_view_wallet": - // TODO: Decide if we want this for arqma - this.restoreViewWallet(params.name, params.password, params.address, params.viewkey, - params.refresh_type, params.refresh_type == "date" ? params.refresh_start_date : params.refresh_start_height) - break - - case "import_wallet": - this.importWallet(params.name, params.password, params.path) - break - - case "open_wallet": - this.openWallet(params.name, params.password) - break - - case "close_wallet": - this.closeWallet() - break - - case "stake": - this.stake(params.password, params.amount, params.key, params.destination) - break - - case "register_service_node": - this.registerSnode(params.password, params.string) - break - - case "unlock_stake": - this.unlockStake(params.password, params.service_node_key, params.confirmed || false) - break - - case "transfer": - this.transfer(params.password, params.amount, params.address, params.payment_id, params.priority, params.note || "", params.address_book) - break - - case "prove_transaction": - this.proveTransaction(params.txid, params.address, params.message) - break - - case "check_transaction": - this.checkTransactionProof(params.signature, params.txid, params.address, params.message) - break - - case "add_address_book": - this.addAddressBook(params.address, params.payment_id, - params.description, params.name, params.starred, - params.hasOwnProperty("index") ? params.index : false - ) - break - - case "delete_address_book": - this.deleteAddressBook(params.hasOwnProperty("index") ? params.index : false) - break - - case "save_tx_notes": - this.saveTxNotes(params.txid, params.note) - break - - case "rescan_blockchain": - this.rescanBlockchain() - break - case "rescan_spent": - this.rescanSpent() - break - case "get_private_keys": - this.getPrivateKeys(params.password) - break - case "export_key_images": - this.exportKeyImages(params.password, params.path) - break - case "import_key_images": - this.importKeyImages(params.password, params.path) - break - - case "change_wallet_password": - this.changeWalletPassword(params.old_password, params.new_password) - break - - case "delete_wallet": - this.deleteWallet(params.password) - break - case "export_transactions": - this.exportTransactions(params) - break - - default: - } - } - isValidPasswordHash (password_hash) { - if (this.wallet_state.password_hash === null) return true - return this.wallet_state.password_hash === password_hash.toString("hex") - } + case "validate_address": + this.validateAddress(params.address) + break - hasPassword () { - if (this.wallet_state.password_hash === null) { - this.sendGateway("set_has_password", false) - return + case "has_password": + this.hasPassword() + break + + case "list_wallets": + this.listWallets() + break + + case "create_wallet": + this.createWallet(params.name, params.password, params.language, params.type) + break + + case "restore_wallet": + this.restoreWallet(params.name, params.password, params.seed, + params.refresh_type, params.refresh_type=="date" ? params.refresh_start_date : params.refresh_start_height) + break + + case "restore_view_wallet": + this.restoreViewWallet(params.name, params.password, params.address, params.viewkey, + params.refresh_type, params.refresh_type=="date" ? params.refresh_start_date : params.refresh_start_height) + break + + case "import_wallet": + this.importWallet(params.name, params.password, params.path) + break + + case "open_wallet": + this.openWallet(params.name, params.password) + break + + case "close_wallet": + this.closeWallet() + break + + case "transfer": + this.transfer(params.password, params.amount, params.address, params.payment_id, params.ringsize, params.priority, params.address_book) + break + + case "prove_transaction": + this.proveTransaction(params.txid, params.address, params.message) + break + + case "check_transaction": + this.checkTransactionProof(params.signature, params.txid, params.address, params.message) + break + + case "add_address_book": + this.addAddressBook(params.address, params.payment_id, + params.description, params.name, params.starred, + params.hasOwnProperty("index") ? params.index : false + ) + break + + case "delete_address_book": + this.deleteAddressBook(params.hasOwnProperty("index") ? params.index : false) + break + + case "save_tx_notes": + this.saveTxNotes(params.txid, params.note) + break + + case "rescan_blockchain": + this.rescanBlockchain() + break + case "rescan_spent": + this.rescanSpent() + break + case "get_private_keys": + this.getPrivateKeys(params.password) + break + case "export_key_images": + this.exportKeyImages(params.password, params.path) + break + case "import_key_images": + this.importKeyImages(params.password, params.path) + break + + case "change_wallet_password": + this.changeWalletPassword(params.old_password, params.new_password) + break + + case "delete_wallet": + this.deleteWallet(params.password) + break + + case "export_transactions": + this.exportTransactions(params) + break + + default: } + } - crypto.pbkdf2("", this.auth[2], 1000, 64, "sha512", (err, password_hash) => { - if (err) { - this.sendGateway("set_has_password", false) - return - } + // validateAddress (address) { + // console.log(address, '<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<') + // this.sendRPC("validate_address", { + // address + // }).then((data) => { + // if (data.hasOwnProperty("error")) { + // this.sendGateway("set_valid_address", { + // address, + // valid: false + // }) + // return + // } - // If the pass hash doesn't match empty string then we don't have a password - this.sendGateway("set_has_password", this.wallet_state.password_hash !== password_hash.toString("hex")) - }) - } + // const { valid, nettype } = data.result - validateAddress (address) { - this.sendRPC("validate_address", { - address - }).then((data) => { - if (data.hasOwnProperty("error")) { - this.sendGateway("set_valid_address", { - address, - valid: false - }) - return - } + // const netMatches = this.net_type === nettype + // const isValid = valid && netMatches - const { valid, nettype } = data.result + // this.sendGateway("set_valid_address", { + // address, + // valid: isValid, + // nettype + // }) + // }) + // } - const netMatches = this.net_type === nettype - const isValid = valid && netMatches + createWallet(filename, password, language, type) { - this.sendGateway("set_valid_address", { - address, - valid: isValid, - nettype - }) - }) - } + let short_address = type == "kurz" - createWallet (filename, password, language) { - // Reset the status error - this.sendGateway("reset_wallet_error") this.sendRPC("create_wallet", { filename, password, - language + language, + short_address }).then((data) => { - if (data.hasOwnProperty("error")) { - this.sendGateway("set_wallet_error", { status: data.error }) + if(data.hasOwnProperty("error")) { + this.sendGateway("set_wallet_error", {status:data.error}) return } @@ -374,75 +307,118 @@ export class WalletRPC { this.wallet_state.open = true this.finalizeNewWallet(filename, true) + }) - } - restoreWallet (filename, password, seed, refresh_type, refresh_start_timestamp_or_height) { - if (refresh_type == "date") { - // Convert timestamp to 00:00 and move back a day - // Core code also moved back some amount of blocks - let timestamp = refresh_start_timestamp_or_height - timestamp = timestamp - (timestamp % 86400000) - 86400000 + } + hasPassword () { + if (this.wallet_state.password_hash === null) { + this.sendGateway("set_has_password", false) + return + } - this.sendGateway("reset_wallet_error") - this.backend.daemon.timestampToHeight(timestamp).then((height) => { - if (height === false) { - this.sendGateway("set_wallet_error", { status: { code: -1, i18n: "notification.errors.invalidRestoreDate" } }) - } else { - this.restoreWallet(filename, password, seed, "height", height) + crypto.pbkdf2("", this.auth[2], 1000, 64, "sha512", (err, password_hash) => { + if (err) { + this.sendGateway("set_has_password", false) + return } - }) - return - } - - let restore_height = refresh_start_timestamp_or_height - if (!Number.isInteger(restore_height)) { - restore_height = 0 + // If the pass hash doesn't match empty string then we don't have a password + this.sendGateway("set_has_password", this.wallet_state.password_hash !== password_hash.toString("hex")) + }) } - seed = seed.trim().replace(/\s{2,}/g, " ") - this.sendGateway("reset_wallet_error") - this.sendRPC("restore_deterministic_wallet", { - filename, - password, - seed, - restore_height + validateAddress (address) { + this.sendRPC("validate_address", { + address }).then((data) => { if (data.hasOwnProperty("error")) { - this.sendGateway("set_wallet_error", { status: data.error }) + this.sendGateway("set_valid_address", { + address, + valid: false + }) return } - // store hash of the password so we can check against it later when requesting private keys, or for sending txs - this.wallet_state.password_hash = crypto.pbkdf2Sync(password, this.auth[2], 1000, 64, "sha512").toString("hex") - this.wallet_state.name = filename - this.wallet_state.open = true + const { valid, nettype } = data.result - this.finalizeNewWallet(filename) + const netMatches = this.net_type === nettype + const isValid = valid && netMatches + + this.sendGateway("set_valid_address", { + address, + valid: isValid, + nettype + }) }) } - restoreViewWallet (filename, password, address, viewkey, refresh_type, refresh_start_timestamp_or_height) { - if (refresh_type == "date") { + restoreWallet (filename, password, seed, refresh_type, refresh_start_timestamp_or_height) { + if (refresh_type == "date") { + // Convert timestamp to 00:00 and move back a day + // Core code also moved back some amount of blocks + let timestamp = refresh_start_timestamp_or_height + timestamp = timestamp - (timestamp % 86400000) - 86400000 + + this.sendGateway("reset_wallet_error") + this.backend.daemon.timestampToHeight(timestamp).then((height) => { + if (height === false) { + this.sendGateway("set_wallet_error", { status: { code: -1, i18n: "notification.errors.invalidRestoreDate" } }) + } else { + this.restoreWallet(filename, password, seed, "height", height) + } + }) + return + } + + let restore_height = refresh_start_timestamp_or_height + + if (!Number.isInteger(restore_height)) { + restore_height = 0 + } + seed = seed.trim().replace(/\s{2,}/g, " ") + + this.sendGateway("reset_wallet_error") + this.sendRPC("restore_deterministic_wallet", { + filename, + password, + seed, + restore_height + }).then((data) => { + if (data.hasOwnProperty("error")) { + this.sendGateway("set_wallet_error", { status: data.error }) + return + } + + // store hash of the password so we can check against it later when requesting private keys, or for sending txs + this.wallet_state.password_hash = crypto.pbkdf2Sync(password, this.auth[2], 1000, 64, "sha512").toString("hex") + this.wallet_state.name = filename + this.wallet_state.open = true + + this.finalizeNewWallet(filename) + }) + } + + restoreViewWallet(filename, password, address, viewkey, refresh_type, refresh_start_timestamp_or_height) { + + if(refresh_type == "date") { // Convert timestamp to 00:00 and move back a day // Core code also moved back some amount of blocks let timestamp = refresh_start_timestamp_or_height timestamp = timestamp - (timestamp % 86400000) - 86400000 this.backend.daemon.timestampToHeight(timestamp).then((height) => { - if (height === false) { - this.sendGateway("set_wallet_error", { status: { code: -1, i18n: "notification.errors.invalidRestoreDate" } }) - } else { + if(height === false) + this.sendGateway("set_wallet_error", {status:{code: -1, message: "Invalid restore date"}}) + else this.restoreViewWallet(filename, password, address, viewkey, "height", height) - } }) return } let refresh_start_height = refresh_start_timestamp_or_height - if (!Number.isInteger(refresh_start_height)) { + if(!Number.isInteger(refresh_start_height)) { refresh_start_height = 0 } @@ -453,8 +429,8 @@ export class WalletRPC { viewkey, refresh_start_height }).then((data) => { - if (data.hasOwnProperty("error")) { - this.sendGateway("set_wallet_error", { status: data.error }) + if(data.hasOwnProperty("error")) { + this.sendGateway("set_wallet_error", {status:data.error}) return } @@ -464,50 +440,47 @@ export class WalletRPC { this.wallet_state.open = true this.finalizeNewWallet(filename) - }) + + }); } - importWallet (filename, password, import_path) { - // Reset the status error - this.sendGateway("reset_wallet_error") + importWallet(filename, password, import_path) { // trim off suffix if exists - if (import_path.endsWith(".keys")) { + if(import_path.endsWith(".keys")) { import_path = import_path.substring(0, import_path.length - ".keys".length) - } else if (import_path.endsWith(".address.txt")) { + } else if(import_path.endsWith(".address.txt")) { import_path = import_path.substring(0, import_path.length - ".address.txt".length) } if (!fs.existsSync(import_path)) { - this.sendGateway("set_wallet_error", { status: { code: -1, i18n: "notification.errors.invalidWalletPath" } }) + this.sendGateway("set_wallet_error", {status:{code: -1, message: "Invalid wallet path"}}) + return } else { + let destination = path.join(this.wallet_dir, filename) - if (fs.existsSync(destination) || fs.existsSync(destination + ".keys")) { - this.sendGateway("set_wallet_error", { status: { code: -1, i18n: "notification.errors.walletAlreadyExists" } }) + if (fs.existsSync(destination) || fs.existsSync(destination+".keys")) { + this.sendGateway("set_wallet_error", {status:{code: -1, message: "Wallet with name already exists"}}) return } - try { - fs.copySync(import_path, destination, fs.constants.COPYFILE_EXCL) + fs.copyFileSync(import_path, destination, fs.constants.COPYFILE_EXCL) - if (fs.existsSync(import_path + ".keys")) { - fs.copySync(import_path + ".keys", destination + ".keys", fs.constants.COPYFILE_EXCL) - } - } catch (e) { - this.sendGateway("set_wallet_error", { status: { code: -1, i18n: "notification.errors.copyWalletFail" } }) - return + if(fs.existsSync(import_path+".keys")) { + fs.copyFileSync(import_path+".keys", destination+".keys", fs.constants.COPYFILE_EXCL) } this.sendRPC("open_wallet", { filename, password }).then((data) => { - if (data.hasOwnProperty("error")) { - if (fs.existsSync(destination)) fs.unlinkSync(destination) - if (fs.existsSync(destination + ".keys")) fs.unlinkSync(destination + ".keys") + if(data.hasOwnProperty("error")) { - this.sendGateway("set_wallet_error", { status: data.error }) + fs.unlinkSync(destination) + fs.unlinkSync(destination+".keys") + + this.sendGateway("set_wallet_error", {status:data.error}) return } @@ -517,20 +490,22 @@ export class WalletRPC { this.wallet_state.open = true this.finalizeNewWallet(filename) - }).catch(() => { - this.sendGateway("set_wallet_error", { status: { code: -1, i18n: "notification.errors.unknownError" } }) + }) + } + } - finalizeNewWallet (filename, newly_created=false) { + finalizeNewWallet(filename, newly_created=false) { + Promise.all([ this.sendRPC("get_address"), this.sendRPC("getheight"), - this.sendRPC("getbalance", { account_index: 0 }), - this.sendRPC("query_key", { key_type: "mnemonic" }), - this.sendRPC("query_key", { key_type: "spend_key" }), - this.sendRPC("query_key", { key_type: "view_key" }) + this.sendRPC("getbalance", {account_index: 0}), + this.sendRPC("query_key", {key_type: "mnemonic"}), + this.sendRPC("query_key", {key_type: "spend_key"}), + this.sendRPC("query_key", {key_type: "view_key"}) ]).then((data) => { let wallet = { info: { @@ -549,20 +524,20 @@ export class WalletRPC { } } for (let n of data) { - if (n.hasOwnProperty("error") || !n.hasOwnProperty("result")) { + if(n.hasOwnProperty("error") || !n.hasOwnProperty("result")) { continue } - if (n.method == "get_address") { + if(n.method == "get_address") { wallet.info.address = n.result.address - } else if (n.method == "getheight") { + } else if(n.method == "getheight") { wallet.info.height = n.result.height } else if (n.method == "getbalance") { wallet.info.balance = n.result.balance wallet.info.unlocked_balance = n.result.unlocked_balance } else if (n.method == "query_key") { wallet.secret[n.params.key_type] = n.result.key - if (n.params.key_type == "spend_key") { - if (/^0*$/.test(n.result.key)) { + if(n.params.key_type == "spend_key") { + if(/^0*$/.test(n.result.key)) { wallet.info.view_only = true } } @@ -570,7 +545,7 @@ export class WalletRPC { } this.saveWallet().then(() => { - let address_txt_path = path.join(this.wallet_dir, filename + ".address.txt") + let address_txt_path = path.join(this.wallet_dir, filename+".address.txt") if (!fs.existsSync(address_txt_path)) { fs.writeFile(address_txt_path, wallet.info.address, "utf8", () => { this.listWallets() @@ -583,24 +558,26 @@ export class WalletRPC { this.sendGateway("set_wallet_data", wallet) this.startHeartbeat() + }) + } - openWallet (filename, password) { - this.sendGateway("reset_wallet_error") + openWallet(filename, password) { + this.sendRPC("open_wallet", { filename, password }).then((data) => { - if (data.hasOwnProperty("error")) { - this.sendGateway("set_wallet_error", { status: data.error }) + if(data.hasOwnProperty("error")) { + this.sendGateway("set_wallet_error", {status:data.error}) return } - let address_txt_path = path.join(this.wallet_dir, filename + ".address.txt") + let address_txt_path = path.join(this.wallet_dir, filename+".address.txt") if (!fs.existsSync(address_txt_path)) { - this.sendRPC("get_address", { account_index: 0 }).then((data) => { - if (data.hasOwnProperty("error") || !data.hasOwnProperty("result")) { + this.sendRPC("get_address", {account_index: 0}).then((data) => { + if(data.hasOwnProperty("error") || !data.hasOwnProperty("result")) { return } fs.writeFile(address_txt_path, data.result.address, "utf8", () => { @@ -617,11 +594,11 @@ export class WalletRPC { this.startHeartbeat() // Check if we have a view only wallet by querying the spend key - this.sendRPC("query_key", { key_type: "spend_key" }).then((data) => { - if (data.hasOwnProperty("error") || !data.hasOwnProperty("result")) { + this.sendRPC("query_key", {key_type: "spend_key"}).then((data) => { + if(data.hasOwnProperty("error") || !data.hasOwnProperty("result")) { return } - if (/^0*$/.test(data.result.key)) { + if(/^0*$/.test(data.result.key)) { this.sendGateway("set_wallet_data", { info: { view_only: true @@ -629,10 +606,11 @@ export class WalletRPC { }) } }) + }) } - startHeartbeat () { + startHeartbeat() { clearInterval(this.heartbeat) this.heartbeat = setInterval(() => { this.heartbeatAction() @@ -735,194 +713,11 @@ export class WalletRPC { }) } - stake (password, amount, service_node_key, destination) { - crypto.pbkdf2(password, this.auth[2], 1000, 64, "sha512", (err, password_hash) => { - if (err) { - this.sendGateway("set_snode_status", { - stake: { - code: -1, - i18n: "notification.errors.internalError", - sending: false - } - }) - return - } - if (!this.isValidPasswordHash(password_hash)) { - this.sendGateway("set_snode_status", { - stake: { - code: -1, - i18n: "notification.errors.invalidPassword", - sending: false - } - }) - return - } - - amount = (parseFloat(amount) * 1e9).toFixed(0) - - this.sendRPC("stake", { - amount, - destination, - service_node_key - }).then((data) => { - if (data.hasOwnProperty("error")) { - let error = data.error.message.charAt(0).toUpperCase() + data.error.message.slice(1) - this.sendGateway("set_snode_status", { - stake: { - code: -1, - message: error, - sending: false - } - }) - return - } - - // Update the new snode list - this.backend.daemon.updateServiceNodes() - - this.sendGateway("set_snode_status", { - stake: { - code: 0, - i18n: "notification.positive.stakeSuccess", - sending: false - } - }) - }) - }) - } - - registerSnode (password, register_service_node_str) { - crypto.pbkdf2(password, this.auth[2], 1000, 64, "sha512", (err, password_hash) => { - if (err) { - this.sendGateway("set_snode_status", { - registration: { - code: -1, - i18n: "notification.errors.internalError", - sending: false - } - }) - return - } - - if (!this.isValidPasswordHash(password_hash)) { - this.sendGateway("set_snode_status", { - registration: { - code: -1, - i18n: "notification.errors.invalidPassword", - sending: false - } - }) - return - } - - this.sendRPC("register_service_node", { - register_service_node_str - }).then((data) => { - if (data.hasOwnProperty("error")) { - const error = data.error.message.charAt(0).toUpperCase() + data.error.message.slice(1) - this.sendGateway("set_snode_status", { - registration: { - code: -1, - message: error, - sending: false - } - }) - return - } - - // Update the new snode list - this.backend.daemon.updateServiceNodes() - - this.sendGateway("set_snode_status", { - registration: { - code: 0, - i18n: "notification.positive.registerServiceNodeSuccess", - sending: false - } - }) - }) - }) - } - - unlockStake (password, service_node_key, confirmed = false) { - const sendError = (message, i18n = true) => { - const key = i18n ? "i18n" : "message" - this.sendGateway("set_snode_status", { - unlock: { - code: -1, - [key]: message, - sending: false - } - }) - } - - // Unlock code 0 means success, 1 means can unlock, -1 means error - crypto.pbkdf2(password, this.auth[2], 1000, 64, "sha512", (err, password_hash) => { - if (err) { - sendError("notification.errors.internalError") - return - } - - if (!this.isValidPasswordHash(password_hash)) { - sendError("notification.errors.invalidPassword") - return - } - - const sendRPC = (path) => { - return this.sendRPC(path, { - service_node_key - }).then(data => { - if (data.hasOwnProperty("error")) { - const error = data.error.message.charAt(0).toUpperCase() + data.error.message.slice(1) - sendError(error, false) - return null - } - - if (!data.hasOwnProperty("result")) { - sendError("notification.errors.failedServiceNodeUnlock") - return null - } - - return data.result - }) - } - - if (confirmed) { - sendRPC("request_stake_unlock").then((data) => { - if (!data) return - - const unlock = { - code: data.unlocked ? 0 : -1, - message: data.msg, - sending: false - } - - // Update the new snode list - if (data.unlocked) { - this.backend.daemon.updateServiceNodes() - } - - this.sendGateway("set_snode_status", { unlock }) - }) - } else { - sendRPC("can_request_stake_unlock").then((data) => { - if (!data) return - - const unlock = { - code: data.can_unlock ? 1 : -1, - message: data.msg, - sending: false - } - - this.sendGateway("set_snode_status", { unlock }) - }) - } - }) - } - transfer (password, amount, address, payment_id, priority, note, address_book = {}) { + console.log(password, amount, address, payment_id, priority, note, address_book) crypto.pbkdf2(password, this.auth[2], 1000, 64, "sha512", (err, password_hash) => { if (err) { + console.log("error") this.sendGateway("set_tx_status", { code: -1, i18n: "notification.errors.internalError", @@ -930,7 +725,9 @@ export class WalletRPC { }) return } - if (!this.isValidPasswordHash(password_hash)) { + //if (!this.isValidPasswordHash(password_hash)) { + if (this.wallet_state.password_hash !== password_hash.toString("hex")) { + console.log('invalidHash') this.sendGateway("set_tx_status", { code: -1, i18n: "notification.errors.invalidPassword", @@ -961,6 +758,7 @@ export class WalletRPC { this.sendRPC(rpc_endpoint, params).then((data) => { if (data.hasOwnProperty("error")) { + console.log('send error') let error = data.error.message.charAt(0).toUpperCase() + data.error.message.slice(1) this.sendGateway("set_tx_status", { code: -1, @@ -969,7 +767,7 @@ export class WalletRPC { }) return } - +console.log('success') this.sendGateway("set_tx_status", { code: 0, i18n: "notification.positive.sendSuccess", @@ -1065,7 +863,6 @@ export class WalletRPC { }) }) } - rescanBlockchain () { this.sendRPC("rescan_blockchain") } @@ -1075,21 +872,23 @@ export class WalletRPC { } getPrivateKeys (password) { + crypto.pbkdf2(password, this.auth[2], 1000, 64, "sha512", (err, password_hash) => { if (err) { this.sendGateway("set_wallet_data", { secret: { - mnemonic: "notification.errors.internalError", + mnemonic: "Internal error", spend_key: -1, view_key: -1 } }) return + return } - if (!this.isValidPasswordHash(password_hash)) { + if(this.wallet_state.password_hash !== password_hash.toString("hex")) { this.sendGateway("set_wallet_data", { secret: { - mnemonic: "notification.errors.invalidPassword", + mnemonic: "Invalid password", spend_key: -1, view_key: -1 } @@ -1097,9 +896,9 @@ export class WalletRPC { return } Promise.all([ - this.sendRPC("query_key", { key_type: "mnemonic" }), - this.sendRPC("query_key", { key_type: "spend_key" }), - this.sendRPC("query_key", { key_type: "view_key" }) + this.sendRPC("query_key", {key_type: "mnemonic"}), + this.sendRPC("query_key", {key_type: "spend_key"}), + this.sendRPC("query_key", {key_type: "view_key"}) ]).then((data) => { let wallet = { secret: { @@ -1109,25 +908,31 @@ export class WalletRPC { } } for (let n of data) { - if (n.hasOwnProperty("error") || !n.hasOwnProperty("result")) { + if(n.hasOwnProperty("error") || !n.hasOwnProperty("result")) { continue } wallet.secret[n.params.key_type] = n.result.key } this.sendGateway("set_wallet_data", wallet) + }) + }) + } - getAddressList () { + + getAddressList() { return new Promise((resolve, reject) => { + Promise.all([ - this.sendRPC("get_address", { account_index: 0 }), - this.sendRPC("getbalance", { account_index: 0 }) + this.sendRPC("get_address", {account_index: 0}), + this.sendRPC("getbalance", {account_index: 0}) ]).then((data) => { + for (let n of data) { - if (n.hasOwnProperty("error") || !n.hasOwnProperty("result")) { + if(n.hasOwnProperty("error") || !n.hasOwnProperty("result")) { resolve({}) return } @@ -1139,8 +944,8 @@ export class WalletRPC { info: { address: data[0].result.address, balance: data[1].result.balance, - unlocked_balance: data[1].result.unlocked_balance - // num_unspent_outputs: data[1].result.num_unspent_outputs + unlocked_balance: data[1].result.unlocked_balance, + //num_unspent_outputs: data[1].result.num_unspent_outputs }, address_list: { primary: [], @@ -1149,14 +954,15 @@ export class WalletRPC { } } - for (let address of data[0].result.addresses) { + for(let address of data[0].result.addresses) { + address.balance = null address.unlocked_balance = null address.num_unspent_outputs = null - if (data[1].result.hasOwnProperty("per_subaddress")) { - for (let address_balance of data[1].result.per_subaddress) { - if (address_balance.address_index == address.address_index) { + if(data[1].result.hasOwnProperty("per_subaddress")) { + for(let address_balance of data[1].result.per_subaddress) { + if(address_balance.address_index == address.address_index) { address.balance = address_balance.balance address.unlocked_balance = address_balance.unlocked_balance address.num_unspent_outputs = address_balance.num_unspent_outputs @@ -1165,9 +971,9 @@ export class WalletRPC { } } - if (address.address_index == 0) { + if(address.address_index == 0) { wallet.address_list.primary.push(address) - } else if (address.used) { + } else if(address.used) { wallet.address_list.used.push(address) } else { wallet.address_list.unused.push(address) @@ -1175,15 +981,15 @@ export class WalletRPC { } // limit to 10 unused addresses - wallet.address_list.unused = wallet.address_list.unused.slice(0, 10) + wallet.address_list.unused = wallet.address_list.unused.slice(0,10) - if (wallet.address_list.unused.length < num_unused_addresses && + if(wallet.address_list.unused.length < num_unused_addresses && !wallet.address_list.primary[0].address.startsWith("ar") && !wallet.address_list.primary[0].address.startsWith("aRi")) { - for (let n = wallet.address_list.unused.length; n < num_unused_addresses; n++) { - this.sendRPC("create_address", { account_index: 0 }).then((data) => { + for(let n = wallet.address_list.unused.length; n < num_unused_addresses; n++) { + this.sendRPC("create_address", {account_index: 0}).then((data) => { wallet.address_list.unused.push(data.result) - if (wallet.address_list.unused.length == num_unused_addresses) { + if(wallet.address_list.unused.length == num_unused_addresses) { // should sort them here resolve(wallet) } @@ -1192,52 +998,64 @@ export class WalletRPC { } else { resolve(wallet) } + }) + }) + } - getTransactions (options = { in: true, out: true, pending: true, failed: true, pool: true }) { + + getTransactions() { return new Promise((resolve, reject) => { - this.sendRPC("get_transfers", options).then((data) => { - if (data.hasOwnProperty("error") || !data.hasOwnProperty("result")) { + this.sendRPC("get_transfers", {in:true,out:true,pending:true,failed:true,pool:true}).then((data) => { + if(data.hasOwnProperty("error") || !data.hasOwnProperty("result")) { resolve({}) return } let wallet = { transactions: { - tx_list: [] + tx_list: [], } } - const types = ["in", "out", "pending", "failed", "pool", "miner", "snode", "gov", "stake"] - types.forEach(type => { - if (data.result.hasOwnProperty(type)) { - wallet.transactions.tx_list = wallet.transactions.tx_list.concat(data.result[type]) - } - }) - - for (let i = 0; i < wallet.transactions.tx_list.length; i++) { - if (/^0*$/.test(wallet.transactions.tx_list[i].payment_id)) { + if(data.result.hasOwnProperty("in")) + wallet.transactions.tx_list = wallet.transactions.tx_list.concat(data.result.in) + if(data.result.hasOwnProperty("out")) + wallet.transactions.tx_list = wallet.transactions.tx_list.concat(data.result.out) + if(data.result.hasOwnProperty("pending")) + wallet.transactions.tx_list = wallet.transactions.tx_list.concat(data.result.pending) + if(data.result.hasOwnProperty("failed")) + wallet.transactions.tx_list = wallet.transactions.tx_list.concat(data.result.failed) + if(data.result.hasOwnProperty("pool")) + wallet.transactions.tx_list = wallet.transactions.tx_list.concat(data.result.pool) + + for(let i = 0; i < wallet.transactions.tx_list.length; i++) { + if(/^0*$/.test(wallet.transactions.tx_list[i].payment_id)) { wallet.transactions.tx_list[i].payment_id = "" - } else if (/^0*$/.test(wallet.transactions.tx_list[i].payment_id.substring(16))) { + } else if(/^0*$/.test(wallet.transactions.tx_list[i].payment_id.substring(16))) { wallet.transactions.tx_list[i].payment_id = wallet.transactions.tx_list[i].payment_id.substring(0, 16) } } - wallet.transactions.tx_list.sort(function (a, b) { - if (a.timestamp < b.timestamp) return 1 - if (a.timestamp > b.timestamp) return -1 + wallet.transactions.tx_list.sort(function(a, b){ + if(a.timestamp < b.timestamp) return 1 + if(a.timestamp > b.timestamp) return -1 return 0 }) + + + resolve(wallet) }) }) } - getAddressBook () { + + getAddressBook() { return new Promise((resolve, reject) => { this.sendRPC("get_address_book").then((data) => { - if (data.hasOwnProperty("error") || !data.hasOwnProperty("result")) { + if(data.hasOwnProperty("error") || !data.hasOwnProperty("result")) { resolve({}) return } @@ -1248,16 +1066,16 @@ export class WalletRPC { } } - if (data.result.entries) { + if(data.result.entries) { let i - for (i = 0; i < data.result.entries.length; i++) { + for(i = 0; i < data.result.entries.length; i++) { let entry = data.result.entries[i] let desc = entry.description.split("::") - if (desc.length == 3) { - entry.starred = desc[0] == "starred" + if(desc.length == 3) { + entry.starred = desc[0] == "starred" ? true : false entry.name = desc[1] entry.description = desc[2] - } else if (desc.length == 2) { + } else if(desc.length == 2) { entry.starred = false entry.name = desc[0] entry.description = desc[1] @@ -1267,24 +1085,28 @@ export class WalletRPC { entry.description = "" } - if (/^0*$/.test(entry.payment_id)) { + if(/^0*$/.test(entry.payment_id)) { entry.payment_id = "" - } else if (/^0*$/.test(entry.payment_id.substring(16))) { + } else if(/^0*$/.test(entry.payment_id.substring(16))) { entry.payment_id = entry.payment_id.substring(0, 16) } - if (entry.starred) { wallet.address_list.address_book_starred.push(entry) } else { wallet.address_list.address_book.push(entry) } + if(entry.starred) + wallet.address_list.address_book_starred.push(entry) + else + wallet.address_list.address_book.push(entry) } } resolve(wallet) + }) }) } - deleteAddressBook (index = false) { - if (index !== false) { - this.sendRPC("delete_address_book", { index: index }).then(() => { + deleteAddressBook(index=false) { + if(index!==false) { + this.sendRPC("delete_address_book", {index:index}).then(() => { this.saveWallet().then(() => { this.getAddressBook().then((data) => { this.sendGateway("set_wallet_data", data) @@ -1294,9 +1116,11 @@ export class WalletRPC { } } - addAddressBook (address, payment_id = null, description = "", name = "", starred = false, index = false) { - if (index !== false) { - this.sendRPC("delete_address_book", { index: index }).then((data) => { + + addAddressBook(address, payment_id=null, description="", name="", starred=false, index=false) { + + if(index!==false) { + this.sendRPC("delete_address_book", {index:index}).then((data) => { this.addAddressBook(address, payment_id, description, name, starred) }) return @@ -1305,11 +1129,12 @@ export class WalletRPC { let params = { address } - if (payment_id != null) { params.payment_id = payment_id } + if(payment_id != null) + params.payment_id = payment_id let desc = [ ] - if (starred) { + if(starred) { desc.push("starred") } desc.push(name, description) @@ -1325,279 +1150,206 @@ export class WalletRPC { }) } - saveTxNotes (txid, note) { - this.sendRPC("set_tx_notes", { txids: [txid], notes: [note] }).then((data) => { + saveTxNotes(txid, note) { + this.sendRPC("set_tx_notes", {txids:[txid], notes:[note]}).then((data) => { this.getTransactions().then((wallet) => { this.sendGateway("set_wallet_data", wallet) }) }) } - exportKeyImages (password, filename = null) { + + exportKeyImages(password, filename=null) { crypto.pbkdf2(password, this.auth[2], 1000, 64, "sha512", (err, password_hash) => { if (err) { - this.sendGateway("show_notification", { type: "negative", i18n: "notification.errors.internalError", timeout: 2000 }) + this.sendGateway("show_notification", {type: "negative", message: "Internal error", timeout: 2000}) return } - if (!this.isValidPasswordHash(password_hash)) { - this.sendGateway("show_notification", { type: "negative", i18n: "notification.errors.invalidPassword", timeout: 2000 }) + if(this.wallet_state.password_hash !== password_hash.toString("hex")) { + this.sendGateway("show_notification", {type: "negative", message: "Invalid password", timeout: 2000}) return } - if (filename == null) { - filename = path.join(this.wallet_data_dir, "images", this.wallet_state.name, "key_image_export") - } else { + if(filename == null) + filename = path.join(this.data_dir, "gui", "key_image_export") + else filename = path.join(filename, "key_image_export") - } - - const onError = () => this.sendGateway("show_notification", { type: "negative", i18n: "notification.errors.keyImages.exporting", timeout: 2000 }) - this.sendRPC("export_key_images").then((data) => { - if (data.hasOwnProperty("error") || !data.hasOwnProperty("result")) { - onError() + this.sendRPC("export_key_images", {filename}).then((data) => { + if(data.hasOwnProperty("error") || !data.hasOwnProperty("result")) { + this.sendGateway("show_notification", {type: "negative", message: "Error exporting key images", timeout: 2000}) return } - if (data.result.signed_key_images) { - fs.outputJSONSync(filename, data.result.signed_key_images) - this.sendGateway("show_notification", { i18n: ["notification.positive.keyImages.exported", { filename }], timeout: 2000 }) - } else { - this.sendGateway("show_notification", { type: "warning", textColor: "black", i18n: "notification.warnings.noKeyImageExport", timeout: 2000 }) - } - }).catch(onError) + this.sendGateway("show_notification", {message: "Key images exported to "+filename, timeout: 2000}) + + }) }) + } - importKeyImages (password, filename = null) { + importKeyImages(password, filename=null) { crypto.pbkdf2(password, this.auth[2], 1000, 64, "sha512", (err, password_hash) => { if (err) { - this.sendGateway("show_notification", { type: "negative", i18n: "notification.errors.internalError", timeout: 2000 }) + this.sendGateway("show_notification", {type: "negative", message: "Internal error", timeout: 2000}) return } - if (!this.isValidPasswordHash(password_hash)) { - this.sendGateway("show_notification", { type: "negative", i18n: "notification.errors.invalidPassword", timeout: 2000 }) + if(this.wallet_state.password_hash !== password_hash.toString("hex")) { + this.sendGateway("show_notification", {type: "negative", message: "Invalid password", timeout: 2000}) return } - if (filename == null) { filename = path.join(this.wallet_data_dir, "images", this.wallet_state.name, "key_image_export") } + if(filename == null) + filename = path.join(this.data_dir, "gui", "key_image_export") - const onError = (i18n) => this.sendGateway("show_notification", { type: "negative", i18n, timeout: 2000 }) - - fs.readJSON(filename).then(signed_key_images => { - this.sendRPC("import_key_images", { signed_key_images }).then((data) => { - if (data.hasOwnProperty("error") || !data.hasOwnProperty("result")) { - onError("notification.errors.keyImages.importing") - return - } + this.sendRPC("import_key_images", {filename}).then((data) => { + if(data.hasOwnProperty("error") || !data.hasOwnProperty("result")) { + this.sendGateway("show_notification", {type: "negative", message: "Error importing key images", timeout: 2000}) + return + } - this.sendGateway("show_notification", { i18n: "notification.positive.keyImages.imported", timeout: 2000 }) - }) - }).catch(() => onError("notification.errors.keyImages.reading")) + this.sendGateway("show_notification", {message: "Key images imported", timeout: 2000}) + }) }) - } - - copyOldGuiWallets (wallets) { - this.sendGateway("set_old_gui_import_status", { code: 1, failed_wallets: [] }) - - const failed_wallets = [] - - for (const wallet of wallets) { - const { type, directory } = wallet - - const old_gui_path = path.join(this.wallet_dir, "old-gui") - const dir_path = path.join(this.wallet_dir, directory) - const stat = fs.statSync(dir_path) - if (!stat.isDirectory()) continue - - // Make sure the directory has the regular and keys file - const wallet_file = path.join(dir_path, directory) - const key_file = wallet_file + ".keys" - - // If we don't have them then don't bother copying - if (!(fs.existsSync(wallet_file) && fs.existsSync(key_file))) { - failed_wallets.push(directory) - continue - } - - // Copy out the file into the relevant directory - const destination = path.join(this.dirs[type], "wallets") - if (!fs.existsSync(destination)) fs.mkdirpSync(destination) - - const new_path = path.join(destination, directory) - try { - // Copy into temp file - if (fs.existsSync(new_path + ".atom") || fs.existsSync(new_path + ".atom.keys")) { - failed_wallets.push(directory) - continue - } - - fs.copyFileSync(wallet_file, new_path + ".atom", fs.constants.COPYFILE_EXCL) - fs.copyFileSync(key_file, new_path + ".atom.keys", fs.constants.COPYFILE_EXCL) - - // Move the folder into a subfolder - if (!fs.existsSync(old_gui_path)) fs.mkdirpSync(old_gui_path) - fs.moveSync(dir_path, path.join(old_gui_path, directory), { overwrite: true }) - } catch (e) { - // Cleanup the copied files if an error - if (fs.existsSync(new_path + ".atom")) fs.unlinkSync(new_path + ".atom") - if (fs.existsSync(new_path + ".atom.keys")) fs.unlinkSync(new_path + ".atom.keys") - failed_wallets.push(directory) - continue - } + } - // Rename the imported wallets if we can - if (!fs.existsSync(new_path) && !fs.existsSync(new_path + ".keys")) { - fs.renameSync(new_path + ".atom", new_path) - fs.renameSync(new_path + ".atom.keys", new_path + ".keys") - } - } - this.sendGateway("set_old_gui_import_status", { code: 0, failed_wallets }) - this.listWallets() - } + listWallets(legacy=false) { - listWallets (legacy = false) { let wallets = { list: [], - directories: [] } fs.readdirSync(this.wallet_dir).forEach(filename => { - switch (filename) { - case ".DS_Store": - case ".DS_Store?": - case "._.DS_Store": - case ".Spotlight-V100": - case ".Trashes": - case "ehthumbs.db": - case "Thumbs.db": - case "old-gui": + if(filename.endsWith(".keys") || + filename.endsWith(".meta.json") || + filename.endsWith(".address.txt") || + filename.endsWith(".bkp-old") || + filename.endsWith(".unportable")) return - } - // If it's a directory then check if it's an old gui wallet - const name = path.join(this.wallet_dir, filename) - const stat = fs.statSync(name) - if (stat.isDirectory()) { - // Make sure the directory has the regular and keys file - const wallet_file = path.join(name, filename) - const key_file = wallet_file + ".keys" - - // If we have them then it is an old gui wallet - if (fs.existsSync(wallet_file) && fs.existsSync(key_file)) { - wallets.directories.push(filename) - } - return + switch(filename) { + case ".DS_Store": + case ".DS_Store?": + case "._.DS_Store": + case ".Spotlight-V100": + case ".Trashes": + case "ehthumbs.db": + case "Thumbs.db": + return } - // Exclude all files with an extension - if (path.extname(filename) !== "") return - let wallet_data = { name: filename, address: null, password_protected: null } - if (fs.existsSync(path.join(this.wallet_dir, filename + ".meta.json"))) { - let meta = fs.readFileSync(path.join(this.wallet_dir, filename + ".meta.json"), "utf8") - if (meta) { + if (fs.existsSync(path.join(this.wallet_dir, filename+".meta.json"))) { + + let meta = fs.readFileSync(path.join(this.wallet_dir, filename+".meta.json"), "utf8") + if(meta) { meta = JSON.parse(meta) wallet_data.address = meta.address wallet_data.password_protected = meta.password_protected } - } else if (fs.existsSync(path.join(this.wallet_dir, filename + ".address.txt"))) { - let address = fs.readFileSync(path.join(this.wallet_dir, filename + ".address.txt"), "utf8") - if (address) { + + } else if (fs.existsSync(path.join(this.wallet_dir, filename+".address.txt"))) { + let address = fs.readFileSync(path.join(this.wallet_dir, filename+".address.txt"), "utf8") + if(address) { wallet_data.address = address } } wallets.list.push(wallet_data) + }) // Check for legacy wallet files - if (legacy) { + if(legacy) { wallets.legacy = [] let legacy_paths = [] - if (os.platform() == "win32") { - legacy_paths = ["C:\\ProgramData\\Arqma"] + if(os.platform() == "win32") { + legacy_paths = ["C:\\ProgramData\\arqma"] } else { - legacy_paths = [path.join(os.homedir(), "Arqma")] + legacy_paths = [path.join(os.homedir(), "Arqma")] } - for (var i = 0; i < legacy_paths.length; i++) { + for(var i = 0; i < legacy_paths.length; i++) { let legacy_config_path = path.join(legacy_paths[i], "config", "wallet_info.json") - if (this.net_type === "test") { legacy_config_path = path.join(legacy_paths[i], "testnet", "config", "wallet_info.json") } - if (!fs.existsSync(legacy_config_path)) { continue } - + if(this.testnet) + legacy_config_path = path.join(legacy_paths[i], "testnet", "config", "wallet_info.json") + if(!fs.existsSync(legacy_config_path)) + continue let legacy_config = JSON.parse(fs.readFileSync(legacy_config_path, "utf8")) let legacy_wallet_path = legacy_config.wallet_filepath - if (!fs.existsSync(legacy_wallet_path)) { continue } - + if(!fs.existsSync(legacy_wallet_path)) + continue let legacy_address = "" - if (fs.existsSync(legacy_wallet_path + ".address.txt")) { - legacy_address = fs.readFileSync(legacy_wallet_path + ".address.txt", "utf8") + if(fs.existsSync(legacy_wallet_path+".address.txt")) { + legacy_address = fs.readFileSync(legacy_wallet_path+".address.txt", "utf8") } - wallets.legacy.push({ path: legacy_wallet_path, address: legacy_address }) + wallets.legacy.push({path: legacy_wallet_path, address: legacy_address}) + } } + this.wallet_list = wallets.list + this.sendGateway("wallet_list", wallets) + } - changeWalletPassword (old_password, new_password) { + changeWalletPassword(old_password, new_password) { crypto.pbkdf2(old_password, this.auth[2], 1000, 64, "sha512", (err, password_hash) => { if (err) { - this.sendGateway("show_notification", { type: "negative", i18n: "notification.errors.internalError", timeout: 2000 }) + this.sendGateway("show_notification", {type: "negative", message: "Internal error", timeout: 2000}) return } - if (!this.isValidPasswordHash(password_hash)) { - this.sendGateway("show_notification", { type: "negative", i18n: "notification.errors.invalidOldPassword", timeout: 2000 }) + if(this.wallet_state.password_hash !== password_hash.toString("hex")) { + this.sendGateway("show_notification", {type: "negative", message: "Invalid old password", timeout: 2000}) return } - this.sendRPC("change_wallet_password", { old_password, new_password }).then((data) => { - if (data.hasOwnProperty("error") || !data.hasOwnProperty("result")) { - this.sendGateway("show_notification", { type: "negative", i18n: "notification.errors.changingPassword", timeout: 2000 }) + this.sendRPC("change_wallet_password", {old_password, new_password}).then((data) => { + if(data.hasOwnProperty("error") || !data.hasOwnProperty("result")) { + this.sendGateway("show_notification", {type: "negative", message: "Error changing password", timeout: 2000}) return } // store hash of the password so we can check against it later when requesting private keys, or for sending txs this.wallet_state.password_hash = crypto.pbkdf2Sync(new_password, this.auth[2], 1000, 64, "sha512").toString("hex") - this.sendGateway("show_notification", { i18n: "notification.positive.passwordUpdated", timeout: 2000 }) + this.sendGateway("show_notification", {message: "Password updated", timeout: 2000}) + }) + }) } - deleteWallet (password) { + deleteWallet(password) { crypto.pbkdf2(password, this.auth[2], 1000, 64, "sha512", (err, password_hash) => { if (err) { - this.sendGateway("show_notification", { type: "negative", i18n: "notification.errors.internalError", timeout: 2000 }) + this.sendGateway("show_notification", {type: "negative", message: "Internal error", timeout: 2000}) return } - if (!this.isValidPasswordHash(password_hash)) { - this.sendGateway("show_notification", { type: "negative", i18n: "notification.errors.invalidPassword", timeout: 2000 }) + if(this.wallet_state.password_hash !== password_hash.toString("hex")) { + this.sendGateway("show_notification", {type: "negative", message: "Invalid password", timeout: 2000}) return } - this.sendGateway("show_loading", { message: "Deleting wallet" }) - let wallet_path = path.join(this.wallet_dir, this.wallet_state.name) this.closeWallet().then(() => { fs.unlinkSync(wallet_path) - fs.unlinkSync(wallet_path + ".keys") - fs.unlinkSync(wallet_path + ".address.txt") - + fs.unlinkSync(wallet_path+".keys") + fs.unlinkSync(wallet_path+".address.txt") this.listWallets() - this.sendGateway("hide_loading") this.sendGateway("return_to_wallet_select") }) }) } - saveWallet () { + saveWallet() { return new Promise((resolve, reject) => { this.sendRPC("store").then(() => { resolve() @@ -1605,7 +1357,7 @@ export class WalletRPC { }) } - closeWallet () { + closeWallet() { return new Promise((resolve, reject) => { clearInterval(this.heartbeat) this.wallet_state = { @@ -1615,6 +1367,9 @@ export class WalletRPC { balance: null, unlocked_balance: null } + this.wallet_info = { + height: 0 + } this.saveWallet().then(() => { this.sendRPC("close_wallet").then(() => { @@ -1624,15 +1379,16 @@ export class WalletRPC { }) } - sendGateway (method, data) { + sendGateway(method, data) { // if wallet is closed, do not send any wallet data to gateway // this is for the case that we close the wallet at the same // after another action has started, but before it has finished - if (!this.wallet_state.open && method == "set_wallet_data") { return } + if(!this.wallet_state.open && method == "set_wallet_data") + return this.backend.send(method, data) } - sendRPC (method, params = {}, timeout = 0) { + sendRPC(method, params={}, timeout=0) { let id = this.id++ let options = { uri: `${this.protocol}${this.hostname}:${this.port}/json_rpc`, @@ -1648,18 +1404,19 @@ export class WalletRPC { sendImmediately: false }, agent: this.agent - } - if (Object.keys(params).length !== 0) { + }; + if(Object.keys(params).length !== 0) { options.json.params = params } - if (timeout > 0) { + if(timeout) { options.timeout = timeout } + //console.log(options) return this.queue.add(() => { return request(options) .then((response) => { - if (response.hasOwnProperty("error")) { + if(response.hasOwnProperty("error")) { return { method: method, params: params, @@ -1685,7 +1442,7 @@ export class WalletRPC { }) } - getRPC (parameter, params = {}) { + getRPC(parameter, params={}) { return this.sendRPC(`get_${parameter}`, params) } diff --git a/src/components/footer.vue b/src/components/footer.vue index 7b71e79..5078081 100644 --- a/src/components/footer.vue +++ b/src/components/footer.vue @@ -1,26 +1,38 @@ @@ -34,23 +46,21 @@ export default { config: state => state.gateway.app.config, daemon: state => state.gateway.daemon, wallet: state => state.gateway.wallet, + pool: state => state.gateway.pool, - config_daemon (state) { - return this.config.daemons[this.config.app.net_type] - }, target_height (state) { - if(this.config_daemon.type === "local") + if(this.config.daemon.type === "local" && !this.daemon.info.is_ready) return Math.max(this.daemon.info.height, this.daemon.info.target_height) else return this.daemon.info.height }, daemon_pct (state) { - if(this.config_daemon.type === "local") + if(this.config.daemon.type === "local") return this.daemon_local_pct return 0 }, daemon_local_pct (state) { - if(this.config_daemon.type === "remote") + if(this.config.daemon.type === "remote") return 0 let pct = (100 * this.daemon.info.height_without_bootstrap / this.target_height).toFixed(1) if(pct == 100.0 && this.daemon.info.height_without_bootstrap < this.target_height) @@ -66,27 +76,41 @@ export default { return Math.min(pct, 100) }, status(state) { - if(this.config_daemon.type === "local") { - if(this.daemon.info.height_without_bootstrap < this.target_height) { - return "syncing" + if(this.config.daemon.type === "local") { + if(this.daemon.info.height_without_bootstrap < this.target_height || !this.daemon.info.is_ready) { + return "Syncing..." } else if(this.wallet.info.height < this.target_height - 1 && this.wallet.info.height != 0) { - return "scanning" + return "Scanning..." } else { - return "ready" + return "Ready" } } else { if(this.wallet.info.height < this.target_height - 1 && this.wallet.info.height != 0) { - return "scanning" - } else if(this.config_daemon.type === "local_remote" && this.daemon.info.height_without_bootstrap < this.target_height) { - return "syncing" + return "Scanning..." + } else if(this.daemon.info.height_without_bootstrap < this.target_height) { + return "Syncing..." } else { - return "ready" + return "Ready" } } return } }), + filters: { + hashrate: (hashrate) => { + if(!hashrate) hashrate = 0 + const byteUnits = [" H/s", " kH/s", " MH/s", " GH/s", " TH/s", " PH/s"] + let i = 0 + if(hashrate > 0) { + while(hashrate > 1000) { + hashrate /= 1000 + i++ + } + } + return parseFloat(hashrate).toFixed(2) + byteUnits[i] + }, + }, data () { return { } diff --git a/src/components/format_bitcoin.vue b/src/components/format_bitcoin.vue index 65f43fd..df87861 100644 --- a/src/components/format_bitcoin.vue +++ b/src/components/format_bitcoin.vue @@ -11,7 +11,7 @@ - + @@ -22,6 +22,7 @@ export default { market: state => state.gateway.market.info, info: state => state.gateway.wallet.info, is_ready (state) { + console.log(this.$store.getters["gateway/isReady"]) return this.$store.getters["gateway/isReady"] } }), diff --git a/src/components/format_ryo.vue b/src/components/format_ryo.vue index 9463672..6c1eaaa 100644 --- a/src/components/format_ryo.vue +++ b/src/components/format_ryo.vue @@ -6,7 +6,7 @@ diff --git a/src/components/pool.vue b/src/components/pool.vue index 91a7375..49cc635 100644 --- a/src/components/pool.vue +++ b/src/components/pool.vue @@ -407,75 +407,6 @@

Point your miner to {{ address_port }} to start solo mining.

-
-
-

- Example configurations: -

-
-
- xmr-stak -
-
- - - -
- -
-
-

Place this config example in pools.txt

-
-
- - - Copy config - - -
-
- -
-"pool_list" :
-[
-    {
-        "pool_address" : "{{ address_port }}",
-        "wallet_address" : "diff.auto",
-        "rig_id" : "Worker_Name",
-        "pool_password" : "x",
-        "use_nicehash" : false,
-        "use_tls" : false,
-        "tls_fingerprint" : "",
-        "pool_weight" : 1
-    },
-],
-"currency" : "cryptonight_gpu",
-                        
- -
-
-
- -
-
- -
-
-
-
-
- @@ -507,7 +438,7 @@