diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7d423db..10e438f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,6 +4,7 @@ on: push: branches: - master + - development jobs: build: @@ -16,28 +17,36 @@ jobs: - name: Checkout git repo uses: actions/checkout@v1 + # Read node version from `.nvmrc` file + - name: Read nvm rc + id: nvmrc + uses: browniebroke/read-nvmrc-action@v1 + - name: Install node uses: actions/setup-node@v1 with: - node-version: "11.9.0" + node-version: ${{ steps.nvmrc.outputs.node_version }} - name: Install dependencies run: npm install - name: Download lokid binaries - run: node ./build/download-binaries.js + run: ./download-asset.sh env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + OS: ${{ runner.os }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + shell: bash + working-directory: ./downloads - name: Extract zip binaries if: runner.os != 'Linux' - run: unzip latest.zip + run: unzip latest shell: bash working-directory: ./downloads - name: Extract xz binaries if: runner.os == 'Linux' - run: tar -xf latest.xz + run: tar -xf latest shell: bash working-directory: ./downloads @@ -51,15 +60,15 @@ jobs: run: ls ./bin shell: bash - - name: Publish window and linux binaries + - name: Build window and linux binaries if: runner.os != 'macOS' - run: npm run release + run: npm run build env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Publish mac binaries + - name: Build mac binaries if: runner.os == 'macOS' - run: npm run release + run: npm run build env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} CSC_LINK: ${{ secrets.MAC_CERTIFICATE }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..8cd5679 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,77 @@ +name: Loki Electron Wallet Release + +on: + push: + branches: + - master + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [windows-latest, macos-latest, ubuntu-latest] + steps: + - name: Checkout git repo + uses: actions/checkout@v1 + + # Read node version from `.nvmrc` file + - name: Read nvm rc + id: nvmrc + uses: browniebroke/read-nvmrc-action@v1 + + - name: Install node + uses: actions/setup-node@v1 + with: + node-version: ${{ steps.nvmrc.outputs.node_version }} + + - name: Install dependencies + run: npm install + + - name: Download lokid binaries + run: ./download-asset.sh + env: + OS: ${{ runner.os }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + shell: bash + working-directory: ./downloads + + - name: Extract zip binaries + if: runner.os != 'Linux' + run: unzip latest.zip + shell: bash + working-directory: ./downloads + + - name: Extract xz binaries + if: runner.os == 'Linux' + run: tar -xf latest.xz + shell: bash + working-directory: ./downloads + + - name: Move lokid binaries + run: | + find ./downloads -type f -name "lokid*" -exec cp '{}' ./bin \; + find ./downloads -type f -name "loki-wallet-rpc*" -exec cp '{}' ./bin \; + shell: bash + + - name: Verify binaries + run: ls ./bin + shell: bash + + - name: Publish window and linux binaries + if: runner.os != 'macOS' + run: npm run release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Publish mac binaries + if: runner.os == 'macOS' + run: npm run release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CSC_LINK: ${{ secrets.MAC_CERTIFICATE }} + CSC_KEY_PASSWORD: ${{ secrets.MAC_CERTIFICATE_PASSWORD }} + SIGNING_APPLE_ID: ${{ secrets.SIGNING_APPLE_ID }} + SIGNING_APP_PASSWORD: ${{ secrets.SIGNING_APP_PASSWORD }} + SIGNING_TEAM_ID: ${{ secrets.SIGNING_TEAM_ID }} diff --git a/.gitignore b/.gitignore index e3ab8b5..a25455d 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,7 @@ bin/* .env -/downloads +downloads/* +!downloads/*.sh dev-app-update.yml diff --git a/.nvmrc b/.nvmrc index ba9aff7..bfbf3ff 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -11.9.0 +14.11.0 diff --git a/.prettierrc.js b/.prettierrc.js index 18d7aa7..65e2cc3 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -1,5 +1 @@ -// Must be included inside .stylintrc, otherwise will conflict and cause weird -// linting errors -module.exports = { - printWidth: 120 -}; +// intentionally blank diff --git a/.stylintrc b/.stylintrc index c623d6a..1f6643e 100644 --- a/.stylintrc +++ b/.stylintrc @@ -32,5 +32,4 @@ "valid": true, "zeroUnits": "never", "zIndexNormalize": false, - "printWidth": 120 } diff --git a/README.md b/README.md index 85c9504..c926627 100644 --- a/README.md +++ b/README.md @@ -23,10 +23,10 @@ Please submit any changes as pull requests to the development branch, all change #### Commands ``` -nvm use 11.9.0 -npm install -g quasar-cli +nvm use 14.11.0 +npm install -g @quasar/cli git clone https://github.com/loki-project/loki-electron-gui-wallet -cd loki-electron-wallet +cd loki-electron-gui-wallet cp path_to_lokid_binaries/lokid bin/ cp path_to_lokid_binaries/loki-wallet-rpc bin/ npm install diff --git a/build/download-binaries.js b/build/download-binaries.js deleted file mode 100644 index e2aa287..0000000 --- a/build/download-binaries.js +++ /dev/null @@ -1,54 +0,0 @@ -const axios = require("axios").default; -const fs = require("fs-extra"); -const path = require("path"); - -async function download() { - const { platform, env } = process; - const repoUrl = "https://api.github.com/repos/loki-project/loki-core/releases/latest"; - try { - const pwd = process.cwd(); - const downloadDir = path.join(pwd, "downloads"); - await fs.ensureDir(downloadDir); - - const headers = { - "Content-Type": "application/json", - "User-Agent": "Loki-Electron-Wallet" - }; - if (env.GH_TOKEN) { - headers.Authorisation = `Bearer ${env.GH_TOKEN}`; - } - - const { data } = await axios.get(repoUrl, { headers }); - const { name } = data; - console.log("Latest release: " + name); - - const url = (data.assets || []) - .map(asset => asset["browser_download_url"]) - .find(url => { - if (platform === "darwin") { - return url.includes("osx") || url.includes("mac"); - } else if (platform === "win32") { - return url.includes("win") || url.includes("windows"); - } - return url.includes("linux"); - }); - - if (!url) { - throw new Error("Download url not found for " + process); - } - console.log("Downloading binary at url: " + url); - - const extension = path.extname(url); - const filePath = path.join(downloadDir, "latest" + extension); - const { data: artifact } = await axios.get(url, { - responseType: "stream" - }); - artifact.pipe(fs.createWriteStream(filePath)); - console.log("Downloaded binary to: " + filePath); - } catch (err) { - console.error("Failed to download file: " + err); - process.exit(1); - } -} - -download(); diff --git a/downloads/download-asset.sh b/downloads/download-asset.sh new file mode 100755 index 0000000..34a8c68 --- /dev/null +++ b/downloads/download-asset.sh @@ -0,0 +1,40 @@ +#!/bin/bash +# Source from: https://github.com/houqp/download-release-assets-action + +set -e + +if [ -z "$OS" ]; then + echo "OS must be set" + exit 1 +fi + +if [ -z "$RENAME" ]; then + RENAME="latest" +fi + +REPO="loki-project/loki-core" +RELEASE="latest" + +if [ "$OS" == "Linux" ]; then + FILE_NAME_REGEX="linux" +elif [ "$OS" == "Windows" ]; then + FILE_NAME_REGEX="win" +elif [ "$OS" == "macOS" ]; then + FILE_NAME_REGEX="osx" +else + echo "OS must be Linux, Windows or macOS" + exit 1 +fi + + +ASSET_URL=$(curl -sL --fail \ + -H "Accept: application/vnd.github.v3+json" \ + -H "Authorization: Bearer ${GITHUB_TOKEN}" \ + "https://api.github.com/repos/${REPO}/releases/${RELEASE}" \ + | jq -r ".assets | .[] | select(.name | test(\"${FILE_NAME_REGEX}\")) | .url") + +curl -sL --fail \ + -H "Accept: application/octet-stream" \ + -H "Authorization: Bearer ${GITHUB_TOKEN}" \ + -o "${RENAME}" \ + "$ASSET_URL" diff --git a/package-lock.json b/package-lock.json index ecbb1a0..1badd9d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "loki-electron-wallet", - "version": "1.4.5", + "version": "1.4.6", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -2671,8 +2671,7 @@ "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, "base": { "version": "0.11.2", @@ -2784,9 +2783,9 @@ } }, "bl": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.2.tgz", - "integrity": "sha512-j4OH8f6Qg2bGuWfRiltT2HYGx0e1QcBTrK9KAHNMwMZdQnDZFk0ZSYIpADjYCB3U12nicC5tVJwSIhwOWjb4RQ==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.3.tgz", + "integrity": "sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg==", "dev": true, "requires": { "buffer": "^5.5.0", @@ -2890,7 +2889,8 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.0.1.tgz", "integrity": "sha512-HRZPIjPcbwAVQvOTxR4YE3o8Xs98NqbbL1iEZDCz7CL8ql0Lt5iOyJFxfnAB0oFs8Oh02F/lLlg30Mexv46LjA==", - "dev": true + "dev": true, + "optional": true }, "boxen": { "version": "4.2.0", @@ -3002,7 +3002,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3247,6 +3246,17 @@ "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", "dev": true }, + "bunyan": { + "version": "1.8.14", + "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.14.tgz", + "integrity": "sha512-LlahJUxXzZLuw/hetUQJmRgZ1LF6+cr5TPpRj6jf327AsiIq2jhYEH4oqUUkVKTor+9w2BT3oxVwhzE5lw9tcg==", + "requires": { + "dtrace-provider": "~0.8", + "moment": "^2.19.3", + "mv": "~2", + "safe-json-stringify": "~1" + } + }, "bytes": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", @@ -4033,8 +4043,7 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "concat-stream": { "version": "1.6.2", @@ -5229,6 +5238,15 @@ "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==", "dev": true }, + "dtrace-provider": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.8.tgz", + "integrity": "sha512-b7Z7cNtHPhH9EJhNNbbeqTcXB8LGFFZhq1PGgEvpeHlzd36bhbdTWoE/Ba/YguqpBSlAPKnARWhVlhunCMwfxg==", + "optional": true, + "requires": { + "nan": "^2.14.0" + } + }, "duplexer": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", @@ -8575,7 +8593,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, "requires": { "once": "^1.3.0", "wrappy": "1" @@ -8584,8 +8601,7 @@ "inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "ini": { "version": "1.3.5", @@ -10074,7 +10090,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, "requires": { "brace-expansion": "^1.1.7" } @@ -10180,8 +10195,7 @@ "moment": { "version": "2.27.0", "resolved": "https://registry.npmjs.org/moment/-/moment-2.27.0.tgz", - "integrity": "sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ==", - "dev": true + "integrity": "sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ==" }, "move-concurrently": { "version": "1.0.1", @@ -10235,11 +10249,45 @@ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", "dev": true }, + "mv": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", + "integrity": "sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI=", + "optional": true, + "requires": { + "mkdirp": "~0.5.1", + "ncp": "~2.0.0", + "rimraf": "~2.4.0" + }, + "dependencies": { + "glob": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", + "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", + "optional": true, + "requires": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "rimraf": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", + "integrity": "sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto=", + "optional": true, + "requires": { + "glob": "^6.0.1" + } + } + } + }, "nan": { "version": "2.14.1", "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz", - "integrity": "sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==", - "dev": true + "integrity": "sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==" }, "nanomatch": { "version": "1.2.13", @@ -10266,6 +10314,12 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, + "ncp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", + "integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=", + "optional": true + }, "negotiator": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", @@ -10906,7 +10960,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, "requires": { "wrappy": "1" } @@ -11253,8 +11306,7 @@ "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, "path-is-inside": { "version": "1.0.2", @@ -12985,6 +13037,12 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, + "safe-json-stringify": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz", + "integrity": "sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg==", + "optional": true + }, "safe-regex": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", @@ -15456,6 +15514,7 @@ "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", "dev": true, + "optional": true, "requires": { "arr-flatten": "^1.1.0", "array-unique": "^0.3.2", @@ -15474,6 +15533,7 @@ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dev": true, + "optional": true, "requires": { "is-extendable": "^0.1.0" } @@ -15506,6 +15566,7 @@ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", "dev": true, + "optional": true, "requires": { "extend-shallow": "^2.0.1", "is-number": "^3.0.0", @@ -15518,6 +15579,7 @@ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dev": true, + "optional": true, "requires": { "is-extendable": "^0.1.0" } @@ -15573,6 +15635,7 @@ "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", "dev": true, + "optional": true, "requires": { "kind-of": "^3.0.2" }, @@ -15582,6 +15645,7 @@ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "dev": true, + "optional": true, "requires": { "is-buffer": "^1.1.5" } @@ -15593,6 +15657,7 @@ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", "dev": true, + "optional": true, "requires": { "arr-diff": "^4.0.0", "array-unique": "^0.3.2", @@ -15642,6 +15707,7 @@ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", "dev": true, + "optional": true, "requires": { "is-number": "^3.0.0", "repeat-string": "^1.6.1" @@ -16608,8 +16674,7 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "write": { "version": "1.0.3", diff --git a/package.json b/package.json index 46dc529..73a3d7b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loki-electron-wallet", - "version": "1.4.5", + "version": "1.4.6", "description": "Modern GUI interface for Loki Currency", "productName": "Loki Electron Wallet", "repository": { @@ -9,13 +9,13 @@ }, "cordovaId": "com.lokinetwork.wallet", "author": { - "name": "Loki", + "name": "Loki Project", "email": "team@loki.network" }, "private": true, "scripts": { "dev": "quasar dev -m electron", - "build": "quasar build -m electron", + "build": "quasar build -m electron --publish=never", "release": "quasar build -m electron --publish=always", "lint": "eslint --fix .", "format": "prettier --write \"**/*.+(js|jsx|json|yml|yaml|css|md|vue)\"", @@ -23,6 +23,7 @@ }, "dependencies": { "axios": "^0.18.1", + "bunyan": "^1.8.14", "electron-is-dev": "^1.0.1", "electron-updater": "^4.2.0", "electron-window-state": "^5.0.3", diff --git a/quasar.conf.js b/quasar.conf.js index 68a3131..5c0b89f 100644 --- a/quasar.conf.js +++ b/quasar.conf.js @@ -158,7 +158,8 @@ module.exports = function() { appId: "com.loki-project.electron-wallet", productName: "Loki Electron Wallet", - copyright: "Copyright © 2018-2019 Loki Project, 2018 Ryo Currency Project", + copyright: + "Copyright © 2018-2020 Loki Project, 2018 Ryo Currency Project", afterSign: "build/notarize.js", artifactName: "loki-electron-wallet-${version}-${os}.${ext}", publish: "github", @@ -192,7 +193,13 @@ module.exports = function() { allowToChangeInstallationDirectory: true }, - files: ["!build/*.js", "!.env", "!dev-app-update.yml"], + files: [ + "!build/*.js", + "!.env", + "!dev-app-update.yml", + "!downloads/**", + "!dist/**" + ], extraResources: ["bin"] } diff --git a/src-electron/main-process/electron-main.js b/src-electron/main-process/electron-main.js index 1fb7d16..fbcc8ca 100644 --- a/src-electron/main-process/electron-main.js +++ b/src-electron/main-process/electron-main.js @@ -26,7 +26,11 @@ let installUpdate = false; const title = `${productName} v${version}`; -const selectionMenu = Menu.buildFromTemplate([{ role: "copy" }, { type: "separator" }, { role: "selectall" }]); +const selectionMenu = Menu.buildFromTemplate([ + { role: "copy" }, + { type: "separator" }, + { role: "selectall" } +]); const inputMenu = Menu.buildFromTemplate([ { role: "cut" }, @@ -54,7 +58,13 @@ function createWindow() { minWidth: 640, minHeight: 480, icon: require("path").join(__statics, "icon_512x512.png"), - title + title, + webPreferences: { + nodeIntegration: true, + nodeIntegrationInWorker: true, + // anything we want preloaded, e.g. global vars + preload: path.resolve(__dirname, "electron-preload.js") + } }); mainWindow.on("close", e => { diff --git a/src-electron/main-process/electron-preload.js b/src-electron/main-process/electron-preload.js new file mode 100644 index 0000000..33beefd --- /dev/null +++ b/src-electron/main-process/electron-preload.js @@ -0,0 +1,3 @@ +const path = require("upath"); + +require(path.resolve(__dirname, "logging.js")); diff --git a/src-electron/main-process/logging.js b/src-electron/main-process/logging.js new file mode 100644 index 0000000..6f63a69 --- /dev/null +++ b/src-electron/main-process/logging.js @@ -0,0 +1,78 @@ +// This allows for logging (to a file) from the frontend by creating global logging functions +// That send ipc calls (from renderer) to the electron main process +// create global logging functions for the frontend. It sends messages to the main +// process which then log to file + +const electron = require("electron"); + +const _ = require("lodash"); + +const ipc = electron.ipcRenderer; + +function log(...args) { + logAtLevel("info", "INFO ", ...args); +} + +if (window.console) { + console._log = console.log; + console.log = log; + console._trace = console.trace; + console._debug = console.debug; + console._info = console.info; + console._warn = console.warn; + console._error = console.error; + console._fatal = console.error; +} + +// To avoid [Object object] in our log since console.log handles non-strings +// smoothly +function cleanArgsForIPC(args) { + const str = args.map(item => { + if (typeof item !== "string") { + try { + return JSON.stringify(item); + } catch (error) { + return item; + } + } + + return item; + }); + + return str.join(" "); +} + +// Backwards-compatible logging, simple strings and no level (defaulted to INFO) +function now() { + const date = new Date(); + return date.toJSON(); +} + +// The Bunyan API: https://github.com/trentm/node-bunyan#log-method-api +function logAtLevel(level, prefix, ...args) { + const fn = `_${level}`; + console[fn](prefix, now(), ...args); + + const logText = cleanArgsForIPC(args); + ipc.send(`log-${level}`, logText); +} + +window.log = { + fatal: _.partial(logAtLevel, "fatal", "FATAL"), + error: _.partial(logAtLevel, "error", "ERROR"), + warn: _.partial(logAtLevel, "warn", "WARN "), + info: _.partial(logAtLevel, "info", "INFO "), + debug: _.partial(logAtLevel, "debug", "DEBUG"), + trace: _.partial(logAtLevel, "trace", "TRACE") +}; + +window.onerror = (message, script, line, col, error) => { + const errorInfo = error && error.stack ? error.stack : JSON.stringify(error); + window.log.error(`Top-level unhandled error: ${errorInfo}`); +}; + +window.addEventListener("unhandledrejection", rejectionEvent => { + const error = rejectionEvent.reason; + const errorInfo = error && error.stack ? error.stack : error; + window.log.error("Top-level unhandled promise rejection:", errorInfo); +}); diff --git a/src-electron/main-process/modules/backend.js b/src-electron/main-process/modules/backend.js index eaa3c27..1212bd6 100644 --- a/src-electron/main-process/modules/backend.js +++ b/src-electron/main-process/modules/backend.js @@ -5,13 +5,19 @@ import { dialog } from "electron"; import semver from "semver"; import axios from "axios"; import { version } from "../../../package.json"; +const bunyan = require("bunyan"); const WebSocket = require("ws"); +const electron = require("electron"); const os = require("os"); const fs = require("fs-extra"); const path = require("upath"); const objectAssignDeep = require("object-assign-deep"); +const { ipcMain: ipc } = electron; + +const LOG_LEVELS = ["fatal", "error", "warn", "info", "debug", "trace"]; + export class Backend { constructor(mainWindow) { this.mainWindow = mainWindow; @@ -24,6 +30,7 @@ export class Backend { this.config_file = null; this.config_data = {}; this.scee = new SCEE(); + this.log = null; } init(config) { @@ -142,7 +149,10 @@ export class Backend { data }; - let encrypted_data = this.scee.encryptString(JSON.stringify(message), this.token); + let encrypted_data = this.scee.encryptString( + JSON.stringify(message), + this.token + ); this.wss.clients.forEach(function each(client) { if (client.readyState === WebSocket.OPEN) { @@ -185,14 +195,22 @@ export class Backend { 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 - }); + 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_init": case "save_config": { @@ -208,12 +226,18 @@ export class Backend { } Object.keys(params).map(key => { - this.config_data[key] = Object.assign(this.config_data[key], params[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])]) + .map(k => [ + k, + this.validate_values(this.config_data[k], this.defaults[k]) + ]) .reduce((map, obj) => { map[obj[0]] = obj[1]; return map; @@ -225,19 +249,24 @@ export class Backend { ...validated }; - fs.writeFile(this.config_file, JSON.stringify(this.config_data, null, 4), "utf8", () => { - if (data.method == "save_config_init") { - this.startup(); - } else { - this.send("set_app_data", { - config: this.config_data, - pending_config: this.config_data - }); - if (config_changed) { - this.send("settings_changed_reboot"); + fs.writeFile( + this.config_file, + JSON.stringify(this.config_data, null, 4), + "utf8", + () => { + if (data.method == "save_config_init") { + this.startup(); + } else { + this.send("set_app_data", { + config: this.config_data, + pending_config: this.config_data + }); + if (config_changed) { + this.send("settings_changed_reboot"); + } } } - }); + ); break; } case "init": @@ -255,7 +284,10 @@ export class Backend { } if (path) { - const baseUrl = net_type === "testnet" ? "https://lokitestnet.com" : "https://lokiblocks.com"; + const baseUrl = + net_type === "testnet" + ? "https://lokitestnet.com" + : "https://lokiblocks.com"; const url = `${baseUrl}/${path}/`; require("electron").shell.openExternal(url + params.id); } @@ -279,12 +311,18 @@ export class Backend { if (err) { this.send("show_notification", { type: "negative", - i18n: ["notification.errors.errorSavingItem", { item: params.type }], + i18n: [ + "notification.errors.errorSavingItem", + { item: params.type } + ], timeout: 2000 }); } else { this.send("show_notification", { - i18n: ["notification.positive.itemSaved", { item: params.type, filename }], + i18n: [ + "notification.positive.itemSaved", + { item: params.type, filename } + ], timeout: 2000 }); } @@ -317,6 +355,36 @@ export class Backend { } } + initLogger(logPath) { + let log = bunyan.createLogger({ + name: "log", + streams: [ + { + type: "rotating-file", + path: path.join(logPath, "electron.log"), + period: "1d", // daily rotation + count: 4 // keep 4 days of logs + } + ] + }); + + LOG_LEVELS.forEach(level => { + ipc.on(`log-${level}`, (first, ...rest) => { + log[level](...rest); + }); + }); + + this.log = log; + + process.on("uncaughtException", error => { + log.error("Unhandled Error", error); + }); + + process.on("unhandledRejection", error => { + log.error("Unhandled Promise Rejection", error); + }); + } + startup() { this.send("set_app_data", { remotes: this.remotes, @@ -344,14 +412,20 @@ export class Backend { if (!this.config_data.hasOwnProperty(key)) { this.config_data[key] = {}; } - this.config_data[key] = Object.assign(this.config_data[key], disk_config_data[key]); + this.config_data[key] = Object.assign( + this.config_data[key], + disk_config_data[key] + ); }); // 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])]) + .map(k => [ + k, + this.validate_values(this.config_data[k], this.defaults[k]) + ]) .reduce((map, obj) => { map[obj[0]] = obj[1]; return map; @@ -364,7 +438,12 @@ export class Backend { }; // 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.writeFile( + this.config_file, + JSON.stringify(this.config_data, null, 4), + "utf8", + () => {} + ); this.send("set_app_data", { config: this.config_data, @@ -427,6 +506,8 @@ export class Backend { fs.mkdirpSync(log_dir); } + this.initLogger(log_dir); + this.daemon = new Daemon(this); this.walletd = new WalletRPC(this); @@ -605,7 +686,11 @@ export class Backend { // 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 isDictionary = v => + typeof v === "object" && + v !== null && + !(v instanceof Array) && + !(v instanceof Date); const modified = { ...values }; // Make sure we have valid defaults @@ -616,7 +701,10 @@ export class Backend { if (!(key in defaults)) continue; const defaultValue = defaults[key]; - const invalidDefault = defaultValue === null || defaultValue === undefined || Number.isNaN(defaultValue); + const invalidDefault = + defaultValue === null || + defaultValue === undefined || + Number.isNaN(defaultValue); if (invalidDefault) continue; const value = modified[key]; @@ -626,7 +714,12 @@ export class Backend { 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)); + const isValidValue = !( + value === undefined || + value === null || + value === "" || + Number.isNaN(value) + ); if (isValidValue) continue; // Otherwise set the default value diff --git a/src-electron/main-process/modules/wallet-rpc.js b/src-electron/main-process/modules/wallet-rpc.js index c0c5ddf..d0af118 100644 --- a/src-electron/main-process/modules/wallet-rpc.js +++ b/src-electron/main-process/modules/wallet-rpc.js @@ -103,7 +103,11 @@ export class WalletRPC { 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"); + const log_file = path.join( + this.dirs[net_type], + "logs", + "wallet-rpc.log" + ); args.push("--log-file", log_file); if (net_type === "testnet") { @@ -125,13 +129,20 @@ export class WalletRPC { this.hostname = "127.0.0.1"; this.port = options.wallet.rpc_bind_port; - const rpcExecutable = process.platform === "win32" ? "loki-wallet-rpc.exe" : "loki-wallet-rpc"; + const rpcExecutable = + process.platform === "win32" + ? "loki-wallet-rpc.exe" + : "loki-wallet-rpc"; // eslint-disable-next-line no-undef const rpcPath = path.join(__ryo_bin, rpcExecutable); // Check if the rpc exists if (!fs.existsSync(rpcPath)) { - reject(new Error("Failed to find Loki Wallet RPC. Please make sure you anti-virus has not removed it.")); + reject( + new Error( + "Failed to find Loki Wallet RPC. Please make sure you anti-virus has not removed it." + ) + ); return; } @@ -140,8 +151,13 @@ export class WalletRPC { .catch(() => "closed") .then(status => { if (status === "closed") { - const options = process.platform === "win32" ? {} : { detached: true }; - this.walletRPCProcess = child_process.spawn(rpcPath, args, options); + const options = + process.platform === "win32" ? {} : { detached: true }; + this.walletRPCProcess = child_process.spawn( + rpcPath, + args, + options + ); this.walletRPCProcess.stdout.on("data", data => { process.stdout.write(`Wallet: ${data}`); @@ -176,7 +192,9 @@ export class WalletRPC { }); } }); - this.walletRPCProcess.on("error", err => process.stderr.write(`Wallet: ${err}`)); + 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; @@ -193,7 +211,11 @@ export class WalletRPC { clearInterval(intrvl); resolve(); } else { - if (this.walletRPCProcess && data.error.cause && data.error.cause.code === "ECONNREFUSED") { + if ( + this.walletRPCProcess && + data.error.cause && + data.error.cause.code === "ECONNREFUSED" + ) { // Ignore } else { clearInterval(intrvl); @@ -251,7 +273,9 @@ export class WalletRPC { params.password, params.seed, params.refresh_type, - params.refresh_type == "date" ? params.refresh_start_date : params.refresh_start_height + params.refresh_type == "date" + ? params.refresh_start_date + : params.refresh_start_height ); break; @@ -263,7 +287,9 @@ export class WalletRPC { params.address, params.viewkey, params.refresh_type, - params.refresh_type == "date" ? params.refresh_start_date : params.refresh_start_height + params.refresh_type == "date" + ? params.refresh_start_date + : params.refresh_start_height ); break; @@ -280,7 +306,12 @@ export class WalletRPC { break; case "stake": - this.stake(params.password, params.amount, params.key, params.destination); + this.stake( + params.password, + params.amount, + params.key, + params.destination + ); break; case "register_service_node": @@ -292,14 +323,31 @@ export class WalletRPC { break; case "unlock_stake": - this.unlockStake(params.password, params.service_node_key, params.confirmed || false); + 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); + this.transfer( + params.password, + params.amount, + params.address, + params.payment_id, + params.priority, + !!params.isSweepAll + ); break; case "relay_tx": - this.relayTransaction(params.metadataList, params.isBlink, params.addressSave, params.note); + this.relayTransaction( + params.metadataList, + params.isBlink, + params.addressSave, + params.note, + !!params.isSweepAll + ); break; case "purchase_lns": this.purchaseLNS( @@ -329,7 +377,12 @@ export class WalletRPC { break; case "check_transaction": - this.checkTransactionProof(params.signature, params.txid, params.address, params.message); + this.checkTransactionProof( + params.signature, + params.txid, + params.address, + params.message + ); break; case "add_address_book": @@ -344,7 +397,9 @@ export class WalletRPC { break; case "delete_address_book": - this.deleteAddressBook(params.hasOwnProperty("index") ? params.index : false); + this.deleteAddressBook( + params.hasOwnProperty("index") ? params.index : false + ); break; case "save_tx_notes": @@ -391,15 +446,25 @@ export class WalletRPC { } // We need to check if the hash generated with an empty string is the same as the password_hash we are storing - crypto.pbkdf2("", this.auth[2], 1000, 64, "sha512", (err, password_hash) => { - if (err) { - this.sendGateway("set_has_password", false); - return; - } + crypto.pbkdf2( + "", + this.auth[2], + 1000, + 64, + "sha512", + (err, password_hash) => { + if (err) { + this.sendGateway("set_has_password", 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")); - }); + // 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") + ); + } + ); } validateAddress(address) { @@ -441,7 +506,9 @@ export class WalletRPC { } // 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.password_hash = crypto + .pbkdf2Sync(password, this.auth[2], 1000, 64, "sha512") + .toString("hex"); this.wallet_state.name = filename; this.wallet_state.open = true; @@ -449,7 +516,13 @@ export class WalletRPC { }); } - restoreWallet(filename, password, seed, refresh_type, refresh_start_timestamp_or_height) { + 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 @@ -492,7 +565,9 @@ export class WalletRPC { } // 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.password_hash = crypto + .pbkdf2Sync(password, this.auth[2], 1000, 64, "sha512") + .toString("hex"); this.wallet_state.name = filename; this.wallet_state.open = true; @@ -500,7 +575,14 @@ export class WalletRPC { }); } - restoreViewWallet(filename, password, address, viewkey, refresh_type, refresh_start_timestamp_or_height) { + 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 @@ -516,7 +598,14 @@ export class WalletRPC { } }); } else { - this.restoreViewWallet(filename, password, address, viewkey, "height", height); + this.restoreViewWallet( + filename, + password, + address, + viewkey, + "height", + height + ); } }); return; @@ -541,7 +630,9 @@ export class WalletRPC { } // 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.password_hash = crypto + .pbkdf2Sync(password, this.auth[2], 1000, 64, "sha512") + .toString("hex"); this.wallet_state.name = filename; this.wallet_state.open = true; @@ -555,9 +646,15 @@ export class WalletRPC { // trim off suffix if exists if (import_path.endsWith(".keys")) { - import_path = import_path.substring(0, import_path.length - ".keys".length); + import_path = import_path.substring( + 0, + import_path.length - ".keys".length + ); } else if (import_path.endsWith(".address.txt")) { - import_path = import_path.substring(0, import_path.length - ".address.txt".length); + import_path = import_path.substring( + 0, + import_path.length - ".address.txt".length + ); } if (!fs.existsSync(import_path)) { @@ -583,7 +680,9 @@ export class WalletRPC { try { fs.copySync(import_path, destination, { errorOnExist: true }); if (fs.existsSync(import_path + ".keys")) { - fs.copySync(import_path + ".keys", destination + ".keys", { errorOnExist: true }); + fs.copySync(import_path + ".keys", destination + ".keys", { + errorOnExist: true + }); } } catch (e) { this.sendGateway("set_wallet_error", { @@ -601,7 +700,8 @@ export class WalletRPC { .then(data => { if (data.hasOwnProperty("error")) { if (fs.existsSync(destination)) fs.unlinkSync(destination); - if (fs.existsSync(destination + ".keys")) fs.unlinkSync(destination + ".keys"); + if (fs.existsSync(destination + ".keys")) + fs.unlinkSync(destination + ".keys"); this.sendGateway("set_wallet_error", { status: data.error }); @@ -672,7 +772,10 @@ 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(); @@ -699,7 +802,10 @@ export class WalletRPC { 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")) { @@ -712,7 +818,9 @@ export class WalletRPC { } // 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.password_hash = crypto + .pbkdf2Sync(password, this.auth[2], 1000, 64, "sha512") + .toString("hex"); this.wallet_state.name = filename; this.wallet_state.open = true; @@ -810,7 +918,8 @@ export class WalletRPC { } this.wallet_state.balance = wallet.info.balance = n.result.balance; - this.wallet_state.unlocked_balance = wallet.info.unlocked_balance = n.result.unlocked_balance; + this.wallet_state.unlocked_balance = wallet.info.unlocked_balance = + n.result.unlocked_balance; this.sendGateway("set_wallet_data", { info: wallet.info }); @@ -849,8 +958,15 @@ export class WalletRPC { async updateLocalLNSRecords() { try { - const addressData = await this.sendRPC("get_address", { account_index: 0 }, 5000); - if (addressData.hasOwnProperty("error") || !addressData.hasOwnProperty("result")) { + const addressData = await this.sendRPC( + "get_address", + { account_index: 0 }, + 5000 + ); + if ( + addressData.hasOwnProperty("error") || + !addressData.hasOwnProperty("result") + ) { return; } @@ -859,19 +975,24 @@ export class WalletRPC { const addresses = results.map(a => a.address).filter(a => !!a); if (addresses.length === 0) return; - const records = await this.backend.daemon.getLNSRecordsForOwners(addresses); + const records = await this.backend.daemon.getLNSRecordsForOwners( + addresses + ); // We need to ensure that we decrypt any incoming records that we already have const currentRecords = this.wallet_state.lnsRecords; const recordsToUpdate = { ...this.purchasedNames }; const newRecords = records.map(record => { // If we have a new record or we haven't decrypted our current record then we should return the new record - const current = currentRecords.find(c => c.name_hash === record.name_hash); + const current = currentRecords.find( + c => c.name_hash === record.name_hash + ); if (!current || !current.name) return record; // We need to check if we need to re-decrypt the record. // This is only necessary if the encrypted_value changed. - const needsToUpdate = current.encrypted_value !== record.encrypted_value; + const needsToUpdate = + current.encrypted_value !== record.encrypted_value; if (needsToUpdate) { const { name, type } = current; recordsToUpdate[name] = type; @@ -914,7 +1035,9 @@ export class WalletRPC { // Update our current records with the new decrypted record const currentRecords = this.wallet_state.lnsRecords; - const isOurRecord = currentRecords.find(c => c.name_hash === record.name_hash); + const isOurRecord = currentRecords.find( + c => c.name_hash === record.name_hash + ); if (!isOurRecord) return null; const newRecords = currentRecords.map(current => { @@ -950,7 +1073,11 @@ export class WalletRPC { if (!record || !record.encrypted_value) return null; // Decrypt the value if possible - const value = await this.decryptLNSValue(type, lowerCaseName, record.encrypted_value); + const value = await this.decryptLNSValue( + type, + lowerCaseName, + record.encrypted_value + ); return { name, @@ -969,7 +1096,9 @@ export class WalletRPC { }); if (data.hasOwnProperty("error")) { - let error = data.error.message.charAt(0).toUpperCase() + data.error.message.slice(1); + let error = + data.error.message.charAt(0).toUpperCase() + + data.error.message.slice(1); throw new Error(error); } @@ -991,7 +1120,9 @@ export class WalletRPC { }); if (data.hasOwnProperty("error")) { - let error = data.error.message.charAt(0).toUpperCase() + data.error.message.slice(1); + let error = + data.error.message.charAt(0).toUpperCase() + + data.error.message.slice(1); throw new Error(error); } @@ -1003,112 +1134,130 @@ 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); + crypto.pbkdf2( + password, + this.auth[2], + 1000, + 64, + "sha512", + (err, password_hash) => { + if (err) { this.sendGateway("set_snode_status", { stake: { code: -1, - message: error, + 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; } - // Update the new snode list - this.backend.daemon.updateServiceNodes(); + amount = (parseFloat(amount) * 1e9).toFixed(0); - this.sendGateway("set_snode_status", { - stake: { - code: 0, - i18n: "notification.positive.stakeSuccess", - sending: false + 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); + crypto.pbkdf2( + password, + this.auth[2], + 1000, + 64, + "sha512", + (err, password_hash) => { + if (err) { this.sendGateway("set_snode_status", { registration: { code: -1, - message: error, + i18n: "notification.errors.internalError", sending: false } }); return; } - // Update the new snode list - this.backend.daemon.updateServiceNodes(); + if (!this.isValidPasswordHash(password_hash)) { + this.sendGateway("set_snode_status", { + registration: { + code: -1, + i18n: "notification.errors.invalidPassword", + sending: false + } + }); + return; + } - this.sendGateway("set_snode_status", { - registration: { - code: 0, - i18n: "notification.positive.registerServiceNodeSuccess", - sending: false + 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 + } + }); }); - }); - }); + } + ); } async updateServiceNodeList() { @@ -1128,74 +1277,92 @@ export class WalletRPC { }; // 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; + 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 }); + }); + } } - - 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 }); - }); - } - }); + ); } // submits the transaction to the blockchain, irreversible from here - async relayTransaction(metadataList, isBlink, addressSave, note) { - const { address, payment_id, address_book } = addressSave; + async relayTransaction(metadataList, isBlink, addressSave, note, isSweepAll) { + // for a sweep these don't exist + let address = ""; + let payment_id = ""; + let address_book = ""; + if (addressSave) { + address = addressSave.address; + payment_id = addressSave.payment_id; + address_book = addressSave.address_book; + } + let failed = false; - let errorMessage = ""; + let errorMessage = "Failed to relay transaction"; // submit each transaction individually for (const hex of metadataList) { @@ -1208,18 +1375,18 @@ export class WalletRPC { try { const data = await this.sendRPC("relay_tx", params); if (data.hasOwnProperty("error")) { - const errMsg = data.error.message; - const error = errMsg.charAt(0).toUpperCase() + errMsg.slice(1); - errorMessage = error; + errorMessage = data.error.message || errorMessage; failed = true; - return; - } - // save note to the new txid - if (data.hasOwnProperty("result")) { + break; + } else if (data.hasOwnProperty("result")) { const tx_hash = data.result.tx_hash; if (note && note !== "") { this.saveTxNotes(tx_hash, note); } + } else { + errorMessage = "Invalid format of relay_tx RPC return message"; + failed = true; + break; } } catch (e) { failed = true; @@ -1227,20 +1394,30 @@ export class WalletRPC { } } + // for updating state on the correct page + const gatewayEndpoint = isSweepAll + ? "set_sweep_all_status" + : "set_tx_status"; + if (!failed) { - this.sendGateway("set_tx_status", { + this.sendGateway(gatewayEndpoint, { code: 0, i18n: "notification.positive.sendSuccess", sending: false }); if (address_book.hasOwnProperty("save") && address_book.save) { - this.addAddressBook(address, payment_id, address_book.description, address_book.name); + this.addAddressBook( + address, + payment_id, + address_book.description, + address_book.name + ); } return; } - this.sendGateway("set_tx_status", { + this.sendGateway(gatewayEndpoint, { code: -1, message: errorMessage, sending: false @@ -1249,7 +1426,8 @@ export class WalletRPC { // prepares params and provides a "confirm" popup to allow the user to check // send address and tx fees before sending - transfer(password, amount, address, payment_id, priority) { + // isSweepAll refers to if it's the sweep from service nodes page + transfer(password, amount, address, payment_id, priority, isSweepAll) { const cryptoCallback = (err, password_hash) => { if (err) { this.sendGateway("set_tx_status", { @@ -1270,12 +1448,15 @@ export class WalletRPC { amount = (parseFloat(amount) * 1e9).toFixed(0); - let sweep_all = amount == this.wallet_state.unlocked_balance; + // if sending "All" the funds, then we need to send all - fee (sweep_all) + // To be amended after the hardfork, v8. + // https://github.com/loki-project/loki-electron-gui-wallet/issues/181 + const isSweepAllRPC = amount == this.wallet_state.unlocked_balance; + const rpc_endpoint = isSweepAllRPC ? "sweep_all" : "transfer_split"; - const rpc_endpoint = sweep_all ? "sweep_all" : "transfer_split"; - const rpcSpecificParams = sweep_all + const rpcSpecificParams = isSweepAllRPC ? { - address: address, + address, account_index: 0 } : { @@ -1292,30 +1473,55 @@ export class WalletRPC { params.payment_id = payment_id; } - this.sendRPC(rpc_endpoint, params).then(data => { - if (data.hasOwnProperty("error")) { - let error = data.error.message.charAt(0).toUpperCase() + data.error.message.slice(1); - this.sendGateway("set_tx_status", { + // for updating state on the correct page + const gatewayEndpoint = isSweepAll + ? "set_sweep_all_status" + : "set_tx_status"; + + this.sendRPC(rpc_endpoint, params) + .then(data => { + if (data.hasOwnProperty("error") || !data.hasOwnProperty("result")) { + let error = ""; + if (data.error && data.error.message) { + error = + data.error.message.charAt(0).toUpperCase() + + data.error.message.slice(1); + } else { + error = `Incorrect result from ${rpc_endpoint} RPC call`; + } + this.sendGateway(gatewayEndpoint, { + code: -1, + message: error, + sending: false + }); + return; + } + + // update state to show a confirm popup + this.sendGateway(gatewayEndpoint, { + code: 1, + i18n: "strings.awaitingConfirmation", + sending: false, + txData: { + // target address for a sweep all + address: data.params.address, + isSweepAll: isSweepAllRPC, + amountList: data.result.amount_list, + metadataList: data.result.tx_metadata_list, + feeList: data.result.fee_list, + priority: data.params.priority, + // for a "send" tx + destinations: data.params.destinations + } + }); + }) + .catch(err => { + this.sendGateway(gatewayEndpoint, { code: -1, - message: error, + message: err.message, sending: false }); - return; - } - // update state to show a confirm popup - this.sendGateway("set_tx_status", { - code: 1, - i18n: "strings.awaitingConfirmation", - sending: false, - txData: { - amountList: data.result.amount_list, - metadataList: data.result.tx_metadata_list, - feeList: data.result.fee_list, - priority: data.params.priority, - destinations: data.params.destinations - } }); - }); }; crypto.pbkdf2(password, this.auth[2], 1000, 64, "sha512", cryptoCallback); @@ -1326,55 +1532,64 @@ export class WalletRPC { const _owner = owner.trim() === "" ? null : owner; const backup_owner = backupOwner.trim() === "" ? null : backupOwner; - crypto.pbkdf2(password, this.auth[2], 1000, 64, "sha512", (err, password_hash) => { - if (err) { - this.sendGateway("set_lns_status", { - code: -1, - i18n: "notification.errors.internalError", - sending: false - }); - return; - } - if (!this.isValidPasswordHash(password_hash)) { - this.sendGateway("set_lns_status", { - code: -1, - i18n: "notification.errors.invalidPassword", - sending: false - }); - return; - } - - const params = { - type, - owner: _owner, - backup_owner, - name: _name, - value - }; - - this.sendRPC("lns_buy_mapping", params).then(data => { - if (data.hasOwnProperty("error")) { - let error = data.error.message.charAt(0).toUpperCase() + data.error.message.slice(1); + crypto.pbkdf2( + password, + this.auth[2], + 1000, + 64, + "sha512", + (err, password_hash) => { + if (err) { this.sendGateway("set_lns_status", { code: -1, - message: error, + i18n: "notification.errors.internalError", + sending: false + }); + return; + } + if (!this.isValidPasswordHash(password_hash)) { + this.sendGateway("set_lns_status", { + code: -1, + i18n: "notification.errors.invalidPassword", sending: false }); return; } - this.purchasedNames[name.trim()] = type; + const params = { + type, + owner: _owner, + backup_owner, + name: _name, + value + }; - // Fetch new records and then get the decrypted record for the one we just inserted - setTimeout(() => this.updateLocalLNSRecords(), 5000); + this.sendRPC("lns_buy_mapping", params).then(data => { + if (data.hasOwnProperty("error")) { + let error = + data.error.message.charAt(0).toUpperCase() + + data.error.message.slice(1); + this.sendGateway("set_lns_status", { + code: -1, + message: error, + sending: false + }); + return; + } - this.sendGateway("set_lns_status", { - code: 0, - i18n: "notification.positive.namePurchased", - sending: false + this.purchasedNames[name.trim()] = type; + + // Fetch new records and then get the decrypted record for the one we just inserted + setTimeout(() => this.updateLocalLNSRecords(), 5000); + + this.sendGateway("set_lns_status", { + code: 0, + i18n: "notification.positive.namePurchased", + sending: false + }); }); - }); - }); + } + ); } updateLNSMapping(password, type, name, value, owner, backupOwner) { @@ -1382,72 +1597,85 @@ export class WalletRPC { const _owner = owner.trim() === "" ? null : owner; const backup_owner = backupOwner.trim() === "" ? null : backupOwner; - crypto.pbkdf2(password, this.auth[2], 1000, 64, "sha512", (err, password_hash) => { - if (err) { - this.sendGateway("set_lns_status", { - code: -1, - i18n: "notification.errors.internalError", - sending: false - }); - return; - } - if (!this.isValidPasswordHash(password_hash)) { - this.sendGateway("set_lns_status", { - code: -1, - i18n: "notification.errors.invalidPassword", - sending: false - }); - return; - } - - const params = { - type, - owner: _owner, - backup_owner, - name: _name, - value - }; - - this.sendRPC("lns_update_mapping", params).then(data => { - if (data.hasOwnProperty("error")) { - let error = data.error.message.charAt(0).toUpperCase() + data.error.message.slice(1); + crypto.pbkdf2( + password, + this.auth[2], + 1000, + 64, + "sha512", + (err, password_hash) => { + if (err) { this.sendGateway("set_lns_status", { code: -1, - message: error, + i18n: "notification.errors.internalError", + sending: false + }); + return; + } + if (!this.isValidPasswordHash(password_hash)) { + this.sendGateway("set_lns_status", { + code: -1, + i18n: "notification.errors.invalidPassword", sending: false }); return; } - this.purchasedNames[name.trim()] = type; + const params = { + type, + owner: _owner, + backup_owner, + name: _name, + value + }; - // Fetch new records and then get the decrypted record for the one we just inserted - setTimeout(() => this.updateLocalLNSRecords(), 5000); - - // Optimistically update our record - const { lnsRecords } = this.wallet_state; - const newRecords = lnsRecords.map(record => { - if (record.type === type && record.name && record.name.toLowerCase() === _name) { - return { - ...record, - owner: _owner, - backup_owner, - value - }; + this.sendRPC("lns_update_mapping", params).then(data => { + if (data.hasOwnProperty("error")) { + let error = + data.error.message.charAt(0).toUpperCase() + + data.error.message.slice(1); + this.sendGateway("set_lns_status", { + code: -1, + message: error, + sending: false + }); + return; } - return record; - }); - this.wallet_state.lnsRecords = newRecords; - this.sendGateway("set_wallet_data", { lnsRecords: newRecords }); + this.purchasedNames[name.trim()] = type; - this.sendGateway("set_lns_status", { - code: 0, - i18n: "notification.positive.lnsRecordUpdated", - sending: false + // Fetch new records and then get the decrypted record for the one we just inserted + setTimeout(() => this.updateLocalLNSRecords(), 5000); + + // Optimistically update our record + const { lnsRecords } = this.wallet_state; + const newRecords = lnsRecords.map(record => { + if ( + record.type === type && + record.name && + record.name.toLowerCase() === _name + ) { + return { + ...record, + owner: _owner, + backup_owner, + value + }; + } + + return record; + }); + this.wallet_state.lnsRecords = newRecords; + this.sendGateway("set_wallet_data", { lnsRecords: newRecords }); + + this.sendGateway("set_lns_status", { + code: 0, + i18n: "notification.positive.lnsRecordUpdated", + sending: false + }); }); - }); - }); + } + ); } proveTransaction(txid, address, message) { @@ -1469,7 +1697,9 @@ export class WalletRPC { this.sendRPC(rpc_endpoint, params).then(data => { if (data.hasOwnProperty("error")) { - let error = data.error.message.charAt(0).toUpperCase() + data.error.message.slice(1); + let error = + data.error.message.charAt(0).toUpperCase() + + data.error.message.slice(1); this.sendGateway("set_prove_transaction_status", { code: -1, message: error, @@ -1509,7 +1739,9 @@ export class WalletRPC { this.sendRPC(rpc_endpoint, params).then(data => { if (data.hasOwnProperty("error")) { - let error = data.error.message.charAt(0).toUpperCase() + data.error.message.slice(1); + let error = + data.error.message.charAt(0).toUpperCase() + + data.error.message.slice(1); this.sendGateway("set_check_transaction_status", { code: -1, message: error, @@ -1538,49 +1770,56 @@ 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", - spend_key: -1, - view_key: -1 - } - }); - return; - } - if (!this.isValidPasswordHash(password_hash)) { - this.sendGateway("set_wallet_data", { - secret: { - mnemonic: "notification.errors.invalidPassword", - spend_key: -1, - view_key: -1 - } - }); - 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" }) - ]).then(data => { - let wallet = { - secret: { - mnemonic: "", - spend_key: "", - view_key: "" - } - }; - for (let n of data) { - if (n.hasOwnProperty("error") || !n.hasOwnProperty("result")) { - continue; - } - wallet.secret[n.params.key_type] = n.result.key; + crypto.pbkdf2( + password, + this.auth[2], + 1000, + 64, + "sha512", + (err, password_hash) => { + if (err) { + this.sendGateway("set_wallet_data", { + secret: { + mnemonic: "notification.errors.internalError", + spend_key: -1, + view_key: -1 + } + }); + return; } + if (!this.isValidPasswordHash(password_hash)) { + this.sendGateway("set_wallet_data", { + secret: { + mnemonic: "notification.errors.invalidPassword", + spend_key: -1, + view_key: -1 + } + }); + 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" }) + ]).then(data => { + let wallet = { + secret: { + mnemonic: "", + spend_key: "", + view_key: "" + } + }; + for (let n of data) { + if (n.hasOwnProperty("error") || !n.hasOwnProperty("result")) { + continue; + } + wallet.secret[n.params.key_type] = n.result.key; + } - this.sendGateway("set_wallet_data", wallet); - }); - }); + this.sendGateway("set_wallet_data", wallet); + }); + } + ); } getAddressList() { @@ -1622,7 +1861,8 @@ export class WalletRPC { 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; + address.num_unspent_outputs = + address_balance.num_unspent_outputs; break; } } @@ -1641,7 +1881,11 @@ export class WalletRPC { wallet.address_list.unused = wallet.address_list.unused.slice(0, 10); if (wallet.address_list.unused.length < num_unused_addresses) { - for (let n = wallet.address_list.unused.length; n < num_unused_addresses; n++) { + for ( + let n = wallet.address_list.unused.length; + n < num_unused_addresses; + n++ + ) { this.sendRPC("create_address", { account_index: 0 }).then(data => { @@ -1678,18 +1922,37 @@ export class WalletRPC { } }; - const types = ["in", "out", "pending", "failed", "pool", "miner", "snode", "gov", "stake"]; + 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]); + 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)) { wallet.transactions.tx_list[i].payment_id = ""; - } 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); + } 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 + ); } } @@ -1745,9 +2008,15 @@ export class WalletRPC { }); for (const entry of addresses) { - const list = entry.starred ? wallet.address_list.address_book_starred : wallet.address_list.address_book; + const list = entry.starred + ? wallet.address_list.address_book_starred + : wallet.address_list.address_book; const hasAddress = list.find(a => { - return a.address === entry.address && a.name === entry.name && a.payment_id === entry.payment_id; + return ( + a.address === entry.address && + a.name === entry.name && + a.payment_id === entry.payment_id + ); }); if (!hasAddress) { list.push(entry); @@ -1771,7 +2040,14 @@ export class WalletRPC { } } - addAddressBook(address, payment_id = null, description = "", name = "", starred = false, index = false) { + addAddressBook( + address, + payment_id = null, + description = "", + name = "", + starred = false, + index = false + ) { if (index !== false) { this.sendRPC("delete_address_book", { index: index }).then(() => { this.addAddressBook(address, payment_id, description, name, starred); @@ -1812,111 +2088,144 @@ export class WalletRPC { } 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 - }); - return; - } - if (!this.isValidPasswordHash(password_hash)) { - this.sendGateway("show_notification", { - type: "negative", - i18n: "notification.errors.invalidPassword", - timeout: 2000 - }); - return; - } + 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 + }); + return; + } + if (!this.isValidPasswordHash(password_hash)) { + this.sendGateway("show_notification", { + type: "negative", + i18n: "notification.errors.invalidPassword", + timeout: 2000 + }); + return; + } - if (filename == null) { - filename = path.join(this.wallet_data_dir, "images", this.wallet_state.name, "key_image_export"); - } else { - filename = path.join(filename, "key_image_export"); - } + if (filename == null) { + filename = path.join( + this.wallet_data_dir, + "images", + this.wallet_state.name, + "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 - }); + 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(); - 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); - }); - } - - 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 - }); - return; - } - if (!this.isValidPasswordHash(password_hash)) { - this.sendGateway("show_notification", { - type: "negative", - i18n: "notification.errors.invalidPassword", - timeout: 2000 - }); - return; - } - - if (filename == null) { - filename = path.join(this.wallet_data_dir, "images", this.wallet_state.name, "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"); + this.sendRPC("export_key_images") + .then(data => { + if ( + data.hasOwnProperty("error") || + !data.hasOwnProperty("result") + ) { + onError(); return; } - this.sendGateway("show_notification", { - i18n: "notification.positive.keyImages.imported", - timeout: 2000 - }); + 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); + } + ); + } + + 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 }); - }) - .catch(() => onError("notification.errors.keyImages.reading")); - }); + return; + } + if (!this.isValidPasswordHash(password_hash)) { + this.sendGateway("show_notification", { + type: "negative", + i18n: "notification.errors.invalidPassword", + timeout: 2000 + }); + return; + } + + if (filename == null) { + filename = path.join( + this.wallet_data_dir, + "images", + this.wallet_state.name, + "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.sendGateway("show_notification", { + i18n: "notification.positive.keyImages.imported", + timeout: 2000 + }); + }); + }) + .catch(() => onError("notification.errors.keyImages.reading")); + } + ); } copyOldGuiWallets(wallets) { @@ -2050,15 +2359,27 @@ export class WalletRPC { password_protected: null }; - if (fs.existsSync(path.join(this.wallet_dir, wallet_name + ".meta.json"))) { - let meta = fs.readFileSync(path.join(this.wallet_dir, wallet_name + ".meta.json"), "utf8"); + if ( + fs.existsSync(path.join(this.wallet_dir, wallet_name + ".meta.json")) + ) { + let meta = fs.readFileSync( + path.join(this.wallet_dir, wallet_name + ".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, wallet_name + ".address.txt"))) { - let address = fs.readFileSync(path.join(this.wallet_dir, wallet_name + ".address.txt"), "utf8"); + } else if ( + fs.existsSync( + path.join(this.wallet_dir, wallet_name + ".address.txt") + ) + ) { + let address = fs.readFileSync( + path.join(this.wallet_dir, wallet_name + ".address.txt"), + "utf8" + ); if (address) { wallet_data.address = address; } @@ -2081,15 +2402,26 @@ export class WalletRPC { } for (var i = 0; i < legacy_paths.length; i++) { try { - let legacy_config_path = path.join(legacy_paths[i], "config", "wallet_info.json"); + 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"); + 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_config = JSON.parse( + fs.readFileSync(legacy_config_path, "utf8") + ); let legacy_wallet_path = legacy_config.wallet_filepath; if (!fs.existsSync(legacy_wallet_path)) { continue; @@ -2097,7 +2429,10 @@ export class WalletRPC { let legacy_address = ""; if (fs.existsSync(legacy_wallet_path + ".address.txt")) { - legacy_address = fs.readFileSync(legacy_wallet_path + ".address.txt", "utf8"); + legacy_address = fs.readFileSync( + legacy_wallet_path + ".address.txt", + "utf8" + ); } wallets.legacy.push({ path: legacy_wallet_path, @@ -2113,88 +2448,104 @@ export class WalletRPC { } 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 - }); - return; - } - if (!this.isValidPasswordHash(password_hash)) { - this.sendGateway("show_notification", { - type: "negative", - i18n: "notification.errors.invalidOldPassword", - timeout: 2000 - }); - return; - } - - this.sendRPC("change_wallet_password", { - old_password, - new_password - }).then(data => { - if (data.hasOwnProperty("error") || !data.hasOwnProperty("result")) { + crypto.pbkdf2( + old_password, + this.auth[2], + 1000, + 64, + "sha512", + (err, password_hash) => { + if (err) { this.sendGateway("show_notification", { type: "negative", - i18n: "notification.errors.changingPassword", + i18n: "notification.errors.internalError", + timeout: 2000 + }); + return; + } + if (!this.isValidPasswordHash(password_hash)) { + this.sendGateway("show_notification", { + type: "negative", + i18n: "notification.errors.invalidOldPassword", 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.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 + }); + return; + } - this.sendGateway("show_notification", { - i18n: "notification.positive.passwordUpdated", - timeout: 2000 + // 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 + }); }); - }); - }); + } + ); } 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 - }); - return; - } - if (!this.isValidPasswordHash(password_hash)) { - this.sendGateway("show_notification", { - type: "negative", - i18n: "notification.errors.invalidPassword", - 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(() => { - try { - if (fs.existsSync(wallet_path + ".keys")) fs.unlinkSync(wallet_path + ".keys"); - if (fs.existsSync(wallet_path + ".address.txt")) fs.unlinkSync(wallet_path + ".address.txt"); - if (fs.existsSync(wallet_path)) fs.unlinkSync(wallet_path); - } catch (e) { - console.warn(`Failed to delete wallet files: ${e}`); + 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 + }); + return; + } + if (!this.isValidPasswordHash(password_hash)) { + this.sendGateway("show_notification", { + type: "negative", + i18n: "notification.errors.invalidPassword", + timeout: 2000 + }); + return; } - this.listWallets(); - this.sendGateway("hide_loading"); - this.sendGateway("return_to_wallet_select"); - }); - }); + this.sendGateway("show_loading", { + message: "Deleting wallet" + }); + + let wallet_path = path.join(this.wallet_dir, this.wallet_state.name); + this.closeWallet().then(() => { + try { + if (fs.existsSync(wallet_path + ".keys")) + fs.unlinkSync(wallet_path + ".keys"); + if (fs.existsSync(wallet_path + ".address.txt")) + fs.unlinkSync(wallet_path + ".address.txt"); + if (fs.existsSync(wallet_path)) fs.unlinkSync(wallet_path); + } catch (e) { + console.warn(`Failed to delete wallet files: ${e}`); + } + + this.listWallets(); + this.sendGateway("hide_loading"); + this.sendGateway("return_to_wallet_select"); + }); + } + ); } async saveWallet() { diff --git a/src/components/confirm_tx_dialog.vue b/src/components/confirm_tx_dialog.vue new file mode 100644 index 0000000..557f1de --- /dev/null +++ b/src/components/confirm_tx_dialog.vue @@ -0,0 +1,75 @@ + + + + + diff --git a/src/components/language_select.vue b/src/components/language_select.vue index e962b9b..0749e34 100644 --- a/src/components/language_select.vue +++ b/src/components/language_select.vue @@ -1,6 +1,8 @@ @@ -57,8 +66,12 @@ export default { }, // just SNs the user has contributed to service_nodes(state) { - const nodes = state.gateway.daemon.service_nodes.nodes; - const getOurContribution = node => node.contributors.find(c => c.address === this.our_address); + let nodes = state.gateway.daemon.service_nodes.nodes; + // don't count reserved nodes in my stakes (where they are a contributor of amount 0) + const getOurContribution = node => + node.contributors.find( + c => c.address === this.our_address && c.amount > 0 + ); return nodes.filter(getOurContribution).map(n => { const ourContribution = getOurContribution(n); return { @@ -219,7 +232,10 @@ export default { }); }, getRole(node) { - const key = node.operator_address === this.our_address ? "strings.operator" : "strings.contributor"; + const key = + node.operator_address === this.our_address + ? "strings.operator" + : "strings.contributor"; return this.$t(key); }, getFee(node) { diff --git a/src/css/app.styl b/src/css/app.styl index 41e8b79..c859719 100644 --- a/src/css/app.styl +++ b/src/css/app.styl @@ -373,6 +373,35 @@ footer, color: white; } } +.confirm-tx-card { + color: "primary"; + width: 450px; + max-width: 450x; + + .confirm-list { + .q-item { + max-height: 100%; + margin-top: 0; + margin-bottom: 4px; + padding-top: 0; + padding-bottom: 0; + } + } + + .label { + color: #cecece; + padding-right: 6px; + } + .address-value { + word-break: break-word; + } + + .confirm-send-btn { + color: white; + background: $positive; + } +} + .header-popover { background: $primary; diff --git a/src/gateway/gateway.js b/src/gateway/gateway.js index f0c2490..3c40ec2 100644 --- a/src/gateway/gateway.js +++ b/src/gateway/gateway.js @@ -13,10 +13,14 @@ export class Gateway extends EventEmitter { this.scee = new SCEE(); // Set the initial language - let language = LocalStorage.has("language") ? LocalStorage.getItem("language") : "en-us"; + let language = LocalStorage.has("language") + ? LocalStorage.getItem("language") + : "en-us"; this.setLanguage(language); - let theme = LocalStorage.has("theme") ? LocalStorage.getItem("theme") : "dark"; + let theme = LocalStorage.has("theme") + ? LocalStorage.getItem("theme") + : "dark"; this.app.store.commit("gateway/set_app_data", { config: { appearance: { @@ -90,7 +94,10 @@ export class Gateway extends EventEmitter { cancel: { flat: true, label: i18n.t("dialog.buttons.cancel"), - color: this.app.store.state.gateway.app.config.appearance.theme === "dark" ? "white" : "dark" + color: + this.app.store.state.gateway.app.config.appearance.theme === "dark" + ? "white" + : "dark" }, dark: this.app.store.state.gateway.app.config.appearance.theme === "dark" }) @@ -111,7 +118,10 @@ export class Gateway extends EventEmitter { method, data }; - let encrypted_data = this.scee.encryptString(JSON.stringify(message), this.token); + let encrypted_data = this.scee.encryptString( + JSON.stringify(message), + this.token + ); this.ws.send(encrypted_data); } @@ -122,7 +132,9 @@ export class Gateway extends EventEmitter { receive(message) { // should wrap this in a try catch, and if fail redirect to error screen // shouldn't happen outside of dev environment - let decrypted_data = JSON.parse(this.scee.decryptString(message, this.token)); + let decrypted_data = JSON.parse( + this.scee.decryptString(message, this.token) + ); if ( typeof decrypted_data !== "object" || @@ -173,6 +185,15 @@ export class Gateway extends EventEmitter { break; } + case "set_sweep_all_status": { + const data = { ...decrypted_data.data }; + if (data.i18n) { + data.message = this.geti18n(data.i18n); + } + this.app.store.commit("gateway/set_sweep_all_status", data); + break; + } + case "set_lns_status": { const data = { ...decrypted_data.data }; if (data.i18n) { @@ -217,7 +238,10 @@ export class Gateway extends EventEmitter { break; } case "set_old_gui_import_status": - this.app.store.commit("gateway/set_old_gui_import_status", decrypted_data.data); + this.app.store.commit( + "gateway/set_old_gui_import_status", + decrypted_data.data + ); break; case "wallet_list": @@ -260,7 +284,10 @@ export class Gateway extends EventEmitter { break; case "set_update_required": - this.app.store.commit("gateway/set_update_required", decrypted_data.data); + this.app.store.commit( + "gateway/set_update_required", + decrypted_data.data + ); break; } } diff --git a/src/i18n/en-us.js b/src/i18n/en-us.js index 9b3f4b8..f817280 100644 --- a/src/i18n/en-us.js +++ b/src/i18n/en-us.js @@ -22,6 +22,8 @@ export default { import: "IMPORT", importWallet: "IMPORT WALLET | IMPORT WALLETS", lns: "LOKI NAME SERVICE", + max: "MAX", + min: "MIN", next: "NEXT", openWallet: "OPEN WALLET", purchase: "PURCHASE", @@ -62,12 +64,14 @@ export default { }, copyAddress: { title: "Copy address", - message: "There is a payment id associated with this address.\nBe sure to copy the payment id separately." + message: + "There is a payment id associated with this address.\nBe sure to copy the payment id separately." }, copyPrivateKeys: { // Copy {seedWords/viewKey/spendKey} title: "Copy {type}", - message: "Be careful who you send your private keys to as they control your funds.", + message: + "Be careful who you send your private keys to as they control your funds.", seedWords: "Seed Words", viewKey: "View Key", spendKey: "Spend Key" @@ -115,7 +119,8 @@ export default { }, rescan: { title: "Rescan wallet", - message: "Warning: Some information about previous transactions\nsuch as the recipient's address will be lost.", + message: + "Warning: Some information about previous transactions\nsuch as the recipient's address will be lost.", ok: "RESCAN" }, restart: { @@ -312,7 +317,8 @@ export default { }, errors: { banningPeer: "Error banning peer", - cannotAccessRemoteNode: "Could not access remote node, please try another remote node", + cannotAccessRemoteNode: + "Could not access remote node, please try another remote node", changingPassword: "Error changing password", copyWalletFail: "Failed to copy wallet", copyingPrivateKeys: "Error copying private keys", @@ -335,8 +341,10 @@ export default { invalidAmount: "Amount not valid", invalidBackupOwner: "Backup owner address not valid", invalidNameLength: "Name must be between 1 and 64 characters long", - invalidNameFormat: "Name may only contain alphanumerics, hyphens and underscore", - invalidNameHypenNotAllowed: "Name may only begin or end with alphanumerics or an underscore", + invalidNameFormat: + "Name may only contain alphanumerics, hyphens and underscore", + invalidNameHypenNotAllowed: + "Name may only begin or end with alphanumerics or an underscore", invalidOldPassword: "Invalid old password", invalidOwner: "Owner address not valid", invalidPassword: "Invalid password", @@ -346,7 +354,8 @@ export default { invalidRestoreDate: "Invalid restore date", invalidRestoreHeight: "Invalid restore height", invalidSeedLength: "Invalid seed word length", - invalidServiceNodeCommand: "Please enter the service node registration command", + invalidServiceNodeCommand: + "Please enter the service node registration command", invalidServiceNodeKey: "Service node key not valid", invalidSessionId: "Session ID not valid", invalidWalletPath: "Invalid wallet path", @@ -384,7 +393,8 @@ export default { mnemonicSeed: "25 (or 24) word mnemonic seed", pasteTransactionId: "Paste transaction ID", pasteTransactionProof: "Paste transaction proof", - proveOptionalMessage: "Optional message against which the signature is signed", + proveOptionalMessage: + "Optional message against which the signature is signed", recipientWalletAddress: "Recipient's wallet address", selectAFile: "Please select a file", sessionId: "The Session ID to link to Loki Name Service", @@ -442,7 +452,8 @@ export default { }, remote: { title: "Remote Daemon Only", - description: "Less security, wallet will connect to a remote node to make all transactions." + description: + "Less security, wallet will connect to a remote node to make all transactions." } }, destinationUnknown: "Destination Unknown", @@ -472,9 +483,11 @@ export default { proveTransactionDescription: "Generate a proof of your incoming/outgoing payment by supplying the transaction ID, the recipient address and an optional message.\nFor the case of outgoing payments, you can get a 'Spend Proof' that proves the authorship of a transaction. In this case, you don't need to specify the recipient address.", readingWalletList: "Reading wallet list", - recentIncomingTransactionsToAddress: "Recent incoming transactions to this address", + recentIncomingTransactionsToAddress: + "Recent incoming transactions to this address", recentTransactionsWithAddress: "Recent transactions with this address", - rescanModalDescription: "Select full rescan or rescan of spent outputs only.", + rescanModalDescription: + "Select full rescan or rescan of spent outputs only.", saveSeedWarning: "Please copy and save these in a secure location!", saveToAddressBook: "Save to address book", seedWords: "Seed words", @@ -483,8 +496,10 @@ export default { "Staking contributes to the safety of the Loki network. For your contribution, you earn LOKI. Once staked, you will have to wait either 15 or 30 days to have your Loki unlocked, depending on if a stake was unlocked by a contributor or the node was deregistered. To learn more about staking, please visit the", serviceNodeRegistrationDescription: 'Enter the {registerCommand} command produced by the daemon that is registering to become a Service Node using the "{prepareCommand}" command', - serviceNodeStartStakingDescription: "To start staking, please visit the Staking tab", - noServiceNodesCurrentlyAvailable: "There are currently no service nodes available for contribution", + serviceNodeStartStakingDescription: + "To start staking, please visit the Staking tab", + noServiceNodesCurrentlyAvailable: + "There are currently no service nodes available for contribution", serviceNodeDetails: { contributors: "Contributors", lastRewardBlockHeight: "Last reward block height", @@ -494,6 +509,7 @@ export default { operatorFee: "Operator Fee", registrationHeight: "Registration height", unlockHeight: "Unlock height", + reserved: "Reserved", serviceNodeKey: "Service Node Key", snKey: "SN Key", stakingRequirement: "Staking requirement", @@ -535,7 +551,8 @@ export default { userNotUsedAddress: "You have not used this address", userUsedAddress: "You have used this address", viewKey: "View key", - viewOnlyMode: "View only mode. Please load full wallet in order to send coins.", + viewOnlyMode: + "View only mode. Please load full wallet in order to send coins.", website: "website" }, titles: { diff --git a/src/mixins/confirm_dialog_mixin.js b/src/mixins/confirm_dialog_mixin.js new file mode 100644 index 0000000..728e3b7 --- /dev/null +++ b/src/mixins/confirm_dialog_mixin.js @@ -0,0 +1,21 @@ +export default { + methods: { + buildDialogFields(val) { + const { feeList, amountList, destinations, metadataList, priority, isSweepAll, address } = val.txData; + const totalFees = feeList.reduce((a, b) => a + b, 0) / 1e9; + const totalAmount = amountList.reduce((a, b) => a + b, 0) / 1e9; + // If the tx is a sweep all, we're sending to the wallet's primary address + // a tx can be split, but only sent to one address + let destination = isSweepAll ? address : destinations[0].address; + const isBlink = [0, 2, 3, 4, 5].includes(priority) ? true : false; + const confirmFields = { + metadataList, + isBlink, + destination, + totalAmount, + totalFees + }; + return confirmFields; + } + } +}; diff --git a/src/mixins/service_node_mixin.js b/src/mixins/service_node_mixin.js new file mode 100644 index 0000000..e8a1b96 --- /dev/null +++ b/src/mixins/service_node_mixin.js @@ -0,0 +1,27 @@ +export default { + methods: { + getMinContribution(node) { + const MAX_NUMBER_OF_CONTRIBUTORS = 4; + // This is calculated in the same way it is calculated on the LokiBlocks site + const openContributionRemaining = this.openForContribution(node); + const minContributionAtomicUnits = + !node.funded && node.contributors.length < MAX_NUMBER_OF_CONTRIBUTORS + ? openContributionRemaining / + (MAX_NUMBER_OF_CONTRIBUTORS - node.contributors.length) + : 0; + const minContributionLoki = minContributionAtomicUnits / 1e9; + // ceiling to 4 decimal places + return minContributionLoki.toFixed(4); + }, + openForContribution(node) { + const openContributionRemaining = + node.staking_requirement > node.total_reserved + ? node.staking_requirement - node.total_reserved + : 0; + return openContributionRemaining; + }, + openForContributionLoki(node) { + return (this.openForContribution(node) / 1e9).toFixed(4); + } + } +}; diff --git a/src/pages/wallet-select/create.vue b/src/pages/wallet-select/create.vue index 22f648d..58ece8d 100644 --- a/src/pages/wallet-select/create.vue +++ b/src/pages/wallet-select/create.vue @@ -1,7 +1,10 @@ @@ -79,7 +87,7 @@ export default { return { wallet: { name: "", - language: languageOptions[0], + language: languageOptions[0].value, password: "", password_confirm: "" }, diff --git a/src/pages/wallet/send.vue b/src/pages/wallet/send.vue index b39ec2d..9aa59c8 100644 --- a/src/pages/wallet/send.vue +++ b/src/pages/wallet/send.vue @@ -10,7 +10,10 @@
- +
- + - + {{ $t("buttons.contacts") }} @@ -67,7 +77,11 @@
- +
- - - -
{{ $t("dialog.confirmTransaction.title") }}
-
- -
-
- {{ $t("dialog.confirmTransaction.sendTo") }}: -
- {{ confirmFields.destination }} -
-
- {{ $t("strings.transactions.amount") }}: - {{ confirmFields.totalAmount }} Loki -
- {{ $t("strings.transactions.fee") }}: {{ confirmFields.totalFees }} Loki -
- {{ $t("dialog.confirmTransaction.priority") }}: - {{ confirmFields.translatedBlinkOrSlow }} -
-
- - - - -
-
+ @@ -186,6 +175,8 @@ import { required, decimal } from "vuelidate/lib/validators"; import { payment_id, address, greater_than_zero } from "src/validators/common"; import LokiField from "components/loki_field"; import WalletPassword from "src/mixins/wallet_password"; +import ConfirmDialogMixin from "src/mixins/confirm_dialog_mixin"; +import ConfirmTransactionDialog from "components/confirm_tx_dialog"; const objectAssignDeep = require("object-assign-deep"); // the case for doing nothing on a tx_status update @@ -193,9 +184,10 @@ const DO_NOTHING = 10; export default { components: { - LokiField + LokiField, + ConfirmTransactionDialog }, - mixins: [WalletPassword], + mixins: [WalletPassword, ConfirmDialogMixin], data() { let priorityOptions = [ { label: this.$t("strings.priorityOptions.blink"), value: 5 }, // Blink @@ -214,8 +206,13 @@ export default { } }, priorityOptions: priorityOptions, - confirmTransaction: false, - confirmFields: {} + confirmFields: { + metadataList: [], + isBlink: false, + totalAmount: -1, + destination: "", + totalFees: 0 + } }; }, computed: mapState({ @@ -233,7 +230,8 @@ export default { const wallet = state.gateway.wallet.info; const prefix = (wallet && wallet.address && wallet.address[0]) || "L"; return `${prefix}..`; - } + }, + confirmTransaction: state => state.gateway.tx_status.code === 1 }), validations: { newTx: { @@ -268,7 +266,7 @@ export default { case DO_NOTHING: break; case 1: - this.buildDialogFields(val); + this.buildDialogFieldsSend(val); break; case 0: this.$q.notify({ @@ -308,7 +306,10 @@ export default { } }, mounted() { - if (this.$route.path == "/wallet/send" && this.$route.query.hasOwnProperty("address")) { + if ( + this.$route.path == "/wallet/send" && + this.$route.query.hasOwnProperty("address") + ) { this.autoFill(this.$route.query); } }, @@ -317,25 +318,9 @@ export default { this.newTx.address = info.address; this.newTx.payment_id = info.payment_id; }, - buildDialogFields(val) { - this.confirmTransaction = true; - const { feeList, amountList, destinations, metadataList, priority } = val.txData; - const totalFees = feeList.reduce((a, b) => a + b, 0) / 1e9; - const totalAmount = amountList.reduce((a, b) => a + b, 0) / 1e9; - // a tx can be split, but only sent to one address - const destination = destinations[0].address; - - const isBlink = [0, 2, 3, 4, 5].includes(priority) ? true : false; - const blinkOrSlow = isBlink ? "strings.priorityOptions.blink" : "strings.priorityOptions.slow"; - const translatedBlinkOrSlow = this.$t(blinkOrSlow); - this.confirmFields = { - metadataList, - isBlink, - translatedBlinkOrSlow, - destination, - totalAmount, - totalFees - }; + buildDialogFieldsSend(txData) { + // build using mixin method + this.confirmFields = this.buildDialogFields(txData); }, onConfirmTransaction() { // put the loading spinner up @@ -366,9 +351,16 @@ export default { note }; + // Commit the transaction this.$gateway.send("wallet", "relay_tx", relayTxData); }, - // helper for constructing a dialog for confirming transactions + onCancelTransaction() { + this.$store.commit("gateway/set_tx_status", { + code: DO_NOTHING, + message: "Cancel the transaction from confirm dialog", + sending: false + }); + }, async send() { this.$v.newTx.$touch(); @@ -454,35 +446,6 @@ export default {