diff --git a/LICENSE b/LICENSE index cca3ee1..8dfe79b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,8 @@ -BSD-3 +Copyright (c) 2018-2020, ArQmA Currency Project +Copyright (c) 2018-2020, Ryo Currency Project +Copyright (c) 2018-2020, Loki Currency Project + +BSD-3 Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: @@ -33,4 +37,3 @@ https://github.com/monero-project/monero/tree/e2c39f6b59fcf5c623c814dfefc518ab0b https://github.com/ryo-currency/ryo-emergency/tree/9d1f51c453978badad21b2feaca2f4348ab26bfa Portions of this wallet have been released by the RYO wallet developers into public domain, we are thankful for their contributions - 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 0000000..0d16864 Binary files /dev/null and b/src-electron/build/ryo-dmg.tiff differ diff --git a/src-electron/icons/icon-32x32.png b/src-electron/icons/icon-32x32.png deleted file mode 100644 index 7d28c09..0000000 Binary files a/src-electron/icons/icon-32x32.png and /dev/null differ diff --git a/src-electron/icons/icon-512x512.png b/src-electron/icons/icon-512x512.png deleted file mode 100644 index 26e6036..0000000 Binary files a/src-electron/icons/icon-512x512.png and /dev/null differ 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 @@