Merge pull request #193 from loki-project/development

v.1.4.6 Bug fixes and service node min/max
This commit is contained in:
kylezs 2020-09-23 09:43:03 +10:00 committed by GitHub
commit 77fa635f0a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 2016 additions and 899 deletions

View File

@ -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 }}

77
.github/workflows/release.yml vendored Normal file
View File

@ -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 }}

3
.gitignore vendored
View File

@ -28,6 +28,7 @@ bin/*
.env
/downloads
downloads/*
!downloads/*.sh
dev-app-update.yml

2
.nvmrc
View File

@ -1 +1 @@
11.9.0
14.11.0

View File

@ -1,5 +1 @@
// Must be included inside .stylintrc, otherwise will conflict and cause weird
// linting errors
module.exports = {
printWidth: 120
};
// intentionally blank

View File

@ -32,5 +32,4 @@
"valid": true,
"zeroUnits": "never",
"zIndexNormalize": false,
"printWidth": 120
}

View File

@ -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

View File

@ -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();

40
downloads/download-asset.sh Executable file
View File

@ -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"

111
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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"]
}

View File

@ -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 => {

View File

@ -0,0 +1,3 @@
const path = require("upath");
require(path.resolve(__dirname, "logging.js"));

View File

@ -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);
});

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,75 @@
<template>
<q-dialog v-model="show" persistent>
<q-card class="confirm-tx-card" dark>
<q-card-section>
<div class="text-h6">{{ $t("dialog.confirmTransaction.title") }}</div>
</q-card-section>
<q-card-section>
<div class="confirm-list">
<div>
<span class="label">{{ $t("dialog.confirmTransaction.sendTo") }}: </span>
<br />
<span class="address-value">{{ sendTo }}</span>
</div>
<br />
<span class="label">{{ $t("strings.transactions.amount") }}: </span>
{{ amount }} Loki
<br />
<span class="label">{{ $t("strings.transactions.fee") }}: </span> {{ fee }} Loki
<br />
<span class="label">{{ $t("dialog.confirmTransaction.priority") }}: </span>
{{ isBlinkToTranslatedLabel(isBlink) }}
</div>
</q-card-section>
<q-card-actions align="right">
<q-btn flat :label="$t('dialog.buttons.cancel')" color="negative" @click="onCancelTransaction" />
<q-btn class="confirm-send-btn" flat :label="$t('buttons.send')" @click="onConfirmTransaction" />
</q-card-actions>
</q-card>
</q-dialog>
</template>
<script>
export default {
name: "ConfirmTransactionDialog",
props: {
sendTo: {
type: String,
required: true
},
amount: {
type: Number,
required: true
},
fee: {
type: Number,
required: true
},
isBlink: {
type: Boolean,
required: true
},
onConfirmTransaction: {
type: Function,
required: true
},
onCancelTransaction: {
type: Function,
required: true
},
show: {
type: Boolean,
required: false,
default: false
}
},
methods: {
isBlinkToTranslatedLabel(isBlink) {
const blinkOrSlow = isBlink ? "strings.priorityOptions.blink" : "strings.priorityOptions.slow";
return this.$t(blinkOrSlow);
}
}
};
</script>
<style></style>

View File

@ -1,6 +1,8 @@
<template>
<div class="language-select column items-center justify-center">
<h6 class="q-my-md" style="font-weight: 300">{{ $t("strings.selectLanguage") }}:</h6>
<h6 class="q-my-md" style="font-weight: 300">
{{ $t("strings.selectLanguage") }}:
</h6>
<div class="row justify-center">
<q-btn
v-for="option in options"

View File

@ -6,13 +6,18 @@
<div class="header">{{ $t("titles.availableForContribution") }}</div>
</div>
<div class="col-md-4">
<q-btn class="float-right vertical-top" icon="refresh" flat @click="updateServiceNodeList" />
<q-btn
class="float-right vertical-top"
icon="refresh"
flat
@click="updateServiceNodeList"
/>
</div>
</div>
<div v-if="awaiting_service_nodes.length > 0">
<div v-if="awaitingServiceNodes.length > 0">
<ServiceNodeList
v-if="awaiting_service_nodes"
:service-nodes="awaiting_service_nodes"
v-if="awaitingServiceNodes"
:service-nodes="awaitingServiceNodes"
button-i18n="buttons.stake"
:details="details"
:action="contributeToNode"
@ -20,7 +25,11 @@
</div>
<div v-else>{{ $t("strings.noServiceNodesCurrentlyAvailable") }}</div>
</div>
<ServiceNodeDetails ref="serviceNodeDetailsContribute" :action="contributeToNode" action-i18n="buttons.stake" />
<ServiceNodeDetails
ref="serviceNodeDetailsContribute"
:action="contributeToNode"
action-i18n="buttons.stake"
/>
<q-inner-loading :showing="fetching" :dark="theme == 'dark'">
<q-spinner color="primary" size="30" />
</q-inner-loading>
@ -37,28 +46,17 @@ export default {
ServiceNodeList,
ServiceNodeDetails
},
props: {
awaitingServiceNodes: {
type: Array,
required: true
}
},
computed: mapState({
awaiting_service_nodes(state) {
const nodes = state.gateway.daemon.service_nodes.nodes;
const isAwaitingContribution = node => !node.active && !node.funded && node.requested_unlock_height === 0;
const compareFee = (n1, n2) => (this.getFeeDecimal(n1) > this.getFeeDecimal(n2) ? 1 : -1);
const awaitingContributionNodes = nodes.filter(isAwaitingContribution).map(n => {
return {
...n,
awaitingContribution: true
};
});
awaitingContributionNodes.sort(compareFee);
return awaitingContributionNodes;
},
theme: state => state.gateway.app.config.appearance.theme,
fetching: state => state.gateway.daemon.service_nodes.fetching
}),
methods: {
getFeeDecimal(node) {
const operatorPortion = node.portions_for_operator;
return (operatorPortion / 18446744073709551612) * 100;
},
scrollToTop() {
window.scrollTo(0, 0);
},

View File

@ -15,7 +15,12 @@
:label="$t(actionI18n)"
@click="action(node, $event)"
/>
<q-btn v-if="can_open" color="primary" :label="$t('buttons.viewOnExplorer')" @click="openExplorer" />
<q-btn
v-if="can_open"
color="primary"
:label="$t('buttons.viewOnExplorer')"
@click="openExplorer"
/>
</q-toolbar>
</q-header>
<q-page-container>
@ -29,27 +34,37 @@
<div class="infoBox">
<div class="infoBoxContent">
<div class="text">
<span>{{ $t("strings.serviceNodeDetails.stakingRequirement") }}</span>
<span>{{
$t("strings.serviceNodeDetails.stakingRequirement")
}}</span>
</div>
<div class="value">
<span><FormatLoki :amount="node.staking_requirement" raw-value/></span>
<span
><FormatLoki :amount="node.staking_requirement" raw-value
/></span>
</div>
</div>
</div>
<div class="infoBox">
<div class="infoBoxContent">
<div class="text">
<span>{{ $t("strings.serviceNodeDetails.totalContributed") }}</span>
<span>{{
$t("strings.serviceNodeDetails.totalContributed")
}}</span>
</div>
<div class="value">
<span><FormatLoki :amount="node.total_contributed" raw-value/></span>
<span
><FormatLoki :amount="node.total_contributed" raw-value
/></span>
</div>
</div>
</div>
<div class="infoBox">
<div class="infoBoxContent">
<div class="text">
<span>{{ $t("strings.serviceNodeDetails.registrationHeight") }}</span>
<span>{{
$t("strings.serviceNodeDetails.registrationHeight")
}}</span>
</div>
<div class="value">
<span>{{ node.registration_height }}</span>
@ -59,7 +74,9 @@
<div class="infoBox">
<div class="infoBoxContent">
<div class="text">
<span>{{ $t("strings.serviceNodeDetails.operatorFee") }}</span>
<span>{{
$t("strings.serviceNodeDetails.operatorFee")
}}</span>
</div>
<div class="value">
<span>{{ operatorFee }}</span>
@ -69,7 +86,9 @@
<div v-if="node.requested_unlock_height > 0" class="infoBox">
<div class="infoBoxContent">
<div class="text">
<span>{{ $t("strings.serviceNodeDetails.unlockHeight") }}</span>
<span>{{
$t("strings.serviceNodeDetails.unlockHeight")
}}</span>
</div>
<div class="value">
<span>{{ node.requested_unlock_height }}</span>
@ -79,7 +98,9 @@
<div class="infoBox">
<div class="infoBoxContent">
<div class="text">
<span>{{ $t("strings.serviceNodeDetails.lastUptimeProof") }}</span>
<span>{{
$t("strings.serviceNodeDetails.lastUptimeProof")
}}</span>
</div>
<div class="value">
<span>{{ formatDate(node.last_uptime_proof * 1000) }}</span>
@ -89,7 +110,9 @@
<div class="infoBox">
<div class="infoBoxContent">
<div class="text">
<span>{{ $t("strings.serviceNodeDetails.lastRewardBlockHeight") }}</span>
<span>{{
$t("strings.serviceNodeDetails.lastRewardBlockHeight")
}}</span>
</div>
<div class="value">
<span>{{ node.last_reward_block_height }}</span>
@ -98,7 +121,11 @@
</div>
</div>
<q-list no-border :dark="theme == 'dark'" class="loki-list">
<q-item-label class="contributors-title">{{ $t("strings.serviceNodeDetails.contributors") }}:</q-item-label>
<q-item-label class="contributors-title"
>{{
$t("strings.serviceNodeDetails.contributors")
}}:</q-item-label
>
<q-item
v-for="contributor in contributors"
:key="contributor.address"
@ -107,20 +134,36 @@
@click="openUserWalletInfo(contributor.address)"
>
<q-item-label>
<q-item-label v-if="isMe(contributor)" class="name non-selectable">{{ $t("strings.me") }}</q-item-label>
<q-item-label v-else class="name non-selectable">{{ contributor.name }}</q-item-label>
<q-item-label class="address ellipsis non-selectable">{{ contributor.address }}</q-item-label>
<q-item-label
v-if="isMe(contributor)"
class="name non-selectable"
>{{ $t("strings.me") }}</q-item-label
>
<q-item-label v-else class="name non-selectable">{{
contributor.name
}}</q-item-label>
<q-item-label class="address ellipsis non-selectable">{{
contributor.address
}}</q-item-label>
<q-item-label class="non-selectable" caption>
<span v-if="isOperator(contributor)">{{ $t("strings.operator") }} </span>
<span v-if="isOperator(contributor)"
>{{ $t("strings.operator") }}
</span>
{{ $t("strings.contribution") }}:
<FormatLoki :amount="contributor.amount" raw-value />
</q-item-label>
</q-item-label>
<ContextMenu :menu-items="menuItems" @copyAddress="copyAddress(contributor.address)" />
<ContextMenu
:menu-items="menuItems"
@copyAddress="copyAddress(contributor.address)"
/>
</q-item>
</q-list>
</div>
<q-inner-loading :showing="unlock_status.sending" :dark="theme == 'dark'">
<q-inner-loading
:showing="unlock_status.sending"
:dark="theme == 'dark'"
>
<q-spinner color="primary" size="30" />
</q-inner-loading>
</q-page-container>
@ -151,7 +194,9 @@ export default {
}
},
data() {
const menuItems = [{ action: "copyAddress", i18n: "menuItems.copyAddress" }];
const menuItems = [
{ action: "copyAddress", i18n: "menuItems.copyAddress" }
];
return {
isVisible: false,
@ -180,7 +225,9 @@ export default {
for (const contributor of this.node.contributors) {
let values = { ...contributor };
const address = address_book.find(a => a.address === contributor.address);
const address = address_book.find(
a => a.address === contributor.address
);
if (address) {
const { name, description } = address;
const separator = description === "" ? "" : " - ";

View File

@ -8,21 +8,38 @@
>
<q-item-section>
<q-item-label class="ellipsis"
>{{ $t("strings.serviceNodeDetails.snKey") }}: {{ node.service_node_pubkey }}</q-item-label
>{{ $t("strings.serviceNodeDetails.snKey") }}:
{{ node.service_node_pubkey }}</q-item-label
>
<q-item-label class="non-selectable">
<span v-if="getRole(node)">{{ getRole(node) }} </span>
<span v-if="node.ourContributionAmount">
{{ $t("strings.contribution") }}: <FormatLoki :amount="node.ourContributionAmount"
/></span>
<span v-if="node.ourContributionAmount > 0">
<span v-if="getRole(node)">{{ getRole(node) }} </span>
<span>
{{ $t("strings.contribution") }}:
<FormatLoki :amount="node.ourContributionAmount" />
</span>
</span>
<!-- you only have a contribution amount of 0 if you are a "contributor"
by way of the node having reserved a spot for you on the node -->
<span
v-if="
node.ourContributionAmount === 0 && node.awaitingContribution
"
>
{{ $t("strings.serviceNodeDetails.reserved") }}
</span>
<span v-if="node.awaitingContribution">
{{ $t("strings.serviceNodeDetails.minContribution") }}: {{ getMinContribution(node) }} LOKI
{{ $t("strings.serviceNodeDetails.maxContribution") }}: {{ openForContributionLoki(node) }} LOKI
{{ $t("strings.serviceNodeDetails.minContribution") }}:
{{ getMinContribution(node) }} LOKI
{{ $t("strings.serviceNodeDetails.maxContribution") }}:
{{ openForContributionLoki(node) }} LOKI
</span>
</q-item-label>
</q-item-section>
<q-item-section v-if="!getRole(node)" side>
<span style="font-size: 16px; color: #cecece"> {{ getFee(node) }} </span>
<q-item-section side>
<span style="font-size: 16px; color: #cecece">{{
getFee(node)
}}</span>
</q-item-section>
<q-item-section side>
<q-btn
@ -56,8 +73,8 @@
import { clipboard } from "electron";
import ContextMenu from "components/menus/contextmenu";
import FormatLoki from "components/format_loki";
const MAX_NUMBER_OF_CONTRIBUTORS = 4;
import ServiceNodeMixin from "src/mixins/service_node_mixin";
import { mapState } from "vuex";
export default {
name: "ServiceNodeList",
@ -65,6 +82,7 @@ export default {
ContextMenu,
FormatLoki
},
mixins: [ServiceNodeMixin],
props: {
serviceNodes: {
type: Array,
@ -92,29 +110,30 @@ export default {
menuItems
};
},
computed: mapState({
our_address: state => {
const primary = state.gateway.wallet.address_list.primary[0];
return (primary && primary.address) || null;
}
}),
methods: {
nodeWithMinContribution(node) {
const nodeWithMinContribution = { ...node, minContribution: this.getMinContribution(node) };
const nodeWithMinContribution = {
...node,
minContribution: this.getMinContribution(node)
};
return nodeWithMinContribution;
},
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);
},
is_ready() {
return this.$store.getters["gateway/isReady"];
},
getRole(node) {
// don't show a role if the user is not an operator or contributor
let role = "";
const opAddress = node.operator_address;
if (node.operator_address === this.our_address) {
if (opAddress === this.our_address) {
role = "strings.operator";
} else if (node.ourContributionAmount && opAddress !== this.our_address) {
// if we're not the operator and we have a contribution amount
role = "strings.contributor";
}
return this.$t(role);
@ -122,21 +141,12 @@ export default {
getNumContributors(node) {
return node.contributors.length;
},
getMinContribution(node) {
// 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);
},
getFee(node) {
const operatorPortion = node.portions_for_operator;
const percentageFee = (operatorPortion / 18446744073709551612) * 100;
return `${percentageFee.toFixed(2)}% ${this.$t("strings.transactions.fee")}`;
return `${percentageFee.toFixed(2)}% ${this.$t(
"strings.transactions.fee"
)}`;
},
copyKey(key) {
clipboard.writeText(key);

View File

@ -3,11 +3,16 @@
<div class="q-px-md q-pt-md">
<p style="color: #cecece">
{{ $t("strings.serviceNodeContributionDescription") }}
<span style="cursor: pointer; text-decoration: underline;" @click="lokiWebsite"
<span
style="cursor: pointer; text-decoration: underline;"
@click="lokiWebsite"
>Loki {{ $t("strings.website") }}.</span
>
</p>
<LokiField :label="$t('fieldLabels.serviceNodeKey')" :error="$v.service_node.key.$error">
<LokiField
:label="$t('fieldLabels.serviceNodeKey')"
:error="$v.service_node.key.$error"
>
<q-input
v-model.trim="service_node.key"
:dark="theme == 'dark'"
@ -17,8 +22,11 @@
@blur="$v.service_node.key.$touch"
/>
</LokiField>
<LokiField :label="$t('fieldLabels.amount')" class="q-mt-md" :error="$v.service_node.amount.$error">
<LokiField
:label="$t('fieldLabels.amount')"
class="q-mt-md"
:error="$v.service_node.amount.$error"
>
<q-input
v-model.trim="service_node.amount"
:dark="theme == 'dark'"
@ -33,12 +41,25 @@
<q-btn
color="secondary"
:text-color="theme == 'dark' ? 'white' : 'dark'"
@click="service_node.amount = unlocked_balance / 1e9"
>{{ $t("buttons.all") }}</q-btn
>
:label="$t('buttons.min')"
:disable="!areButtonsEnabled()"
@click="service_node.amount = minStake(service_node.key)"
/>
<q-btn
color="secondary"
:text-color="theme == 'dark' ? 'white' : 'dark'"
:label="$t('buttons.max')"
:disable="!areButtonsEnabled()"
@click="service_node.amount = maxStake(service_node.key)"
/>
</LokiField>
<div class="submit-button">
<q-btn :disable="!is_able_to_send" color="primary" :label="$t('buttons.stake')" @click="stake()" />
<q-btn
:disable="!is_able_to_send"
color="primary"
:label="$t('buttons.stake')"
@click="stake()"
/>
<q-btn
:disable="!is_able_to_send"
color="secondary"
@ -47,8 +68,24 @@
/>
</div>
</div>
<ServiceNodeContribute class="contribute" @contribute="fillStakingFields" />
<q-inner-loading :showing="stake_status.sending || tx_status.sending" :dark="theme == 'dark'">
<ServiceNodeContribute
:awaiting-service-nodes="awaiting_service_nodes"
class="contribute"
@contribute="fillStakingFields"
/>
<ConfirmTransactionDialog
:show="confirmSweepAll"
:amount="confirmFields.totalAmount"
:is-blink="confirmFields.isBlink"
:send-to="confirmFields.destination"
:fee="confirmFields.totalFees"
:on-confirm-transaction="onConfirmTransaction"
:on-cancel-transaction="onCancelTransaction"
/>
<q-inner-loading
:showing="stake_status.sending || sweep_all_status.sending"
:dark="theme == 'dark'"
>
<q-spinner color="primary" size="30" />
</q-inner-loading>
</div>
@ -61,20 +98,37 @@ import { required, decimal } from "vuelidate/lib/validators";
import { service_node_key, 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 ServiceNodeContribute from "./service_node_contribute";
import ServiceNodeMixin from "src/mixins/service_node_mixin";
import ConfirmTransactionDialog from "components/confirm_tx_dialog";
const DO_NOTHING = 10;
export default {
name: "ServiceNodeStaking",
components: {
LokiField,
ServiceNodeContribute
ServiceNodeContribute,
ConfirmTransactionDialog
},
mixins: [WalletPassword],
mixins: [WalletPassword, ConfirmDialogMixin, ServiceNodeMixin],
data() {
return {
service_node: {
key: "",
amount: 0
amount: 0,
// the min and max are for that particular SN,
// start at min/max for the wallet
minStakeAmount: 0,
maxStakeAmount: this.unlocked_balance / 1e9
},
confirmFields: {
metadataList: [],
isBlink: false,
totalAmount: -1,
destination: "",
totalFees: 0
}
};
},
@ -83,8 +137,9 @@ export default {
unlocked_balance: state => state.gateway.wallet.info.unlocked_balance,
info: state => state.gateway.wallet.info,
stake_status: state => state.gateway.service_node_status.stake,
tx_status: state => state.gateway.tx_status,
sweep_all_status: state => state.gateway.sweep_all_status,
award_address: state => state.gateway.wallet.info.address,
confirmSweepAll: state => state.gateway.sweep_all_status.code === 1,
is_ready() {
return this.$store.getters["gateway/isReady"];
},
@ -95,6 +150,49 @@ export default {
const wallet = state.gateway.wallet.info;
const prefix = (wallet && wallet.address && wallet.address[0]) || "L";
return `${prefix}..`;
},
awaiting_service_nodes(state) {
const nodes = state.gateway.daemon.service_nodes.nodes;
// a reserved node is one on which someone is a "contributor" of amount = 0
const getOurContribution = node =>
node.contributors.find(
c => c.address === this.our_address && c.amount > 0
);
const isAwaitingContribution = node =>
!node.active && !node.funded && node.requested_unlock_height === 0;
const isAwaitingContributionNonReserved = node =>
isAwaitingContribution(node) && !getOurContribution(node);
const isAwaitingContributionReserved = node =>
isAwaitingContribution(node) && getOurContribution(node);
// we want the reserved nodes sorted by fee at the top
const awaitingContributionNodesReserved = nodes
.filter(isAwaitingContributionReserved)
.map(n => {
return {
...n,
awaitingContribution: true
};
});
const awaitingContributionNodesNonReserved = nodes
.filter(isAwaitingContributionNonReserved)
.map(n => {
return {
...n,
awaitingContribution: true
};
});
const compareFee = (n1, n2) =>
this.getFeeDecimal(n1) > this.getFeeDecimal(n2) ? 1 : -1;
awaitingContributionNodesReserved.sort(compareFee);
awaitingContributionNodesNonReserved.sort(compareFee);
const nodesForContribution = [
...awaitingContributionNodesReserved,
...awaitingContributionNodesNonReserved
];
return nodesForContribution;
}
}),
validations: {
@ -136,22 +234,44 @@ export default {
},
deep: true
},
tx_status: {
sweep_all_status: {
handler(val, old) {
if (val.code == old.code) return;
switch (this.tx_status.code) {
const { code, message } = val;
switch (code) {
// the "nothing", so we can update state without doing anything
// in particular
case DO_NOTHING:
break;
case 1:
this.buildDialogFieldsSweepAll(val);
break;
case 0:
this.$q.notify({
type: "positive",
timeout: 1000,
message: this.tx_status.message
message
});
this.$v.$reset();
this.newTx = {
amount: 0,
address: "",
payment_id: "",
// blink
priority: 5,
address_book: {
save: false,
name: "",
description: ""
},
note: ""
};
break;
case -1:
this.$q.notify({
type: "negative",
timeout: 3000,
message: this.tx_status.message
message
});
break;
}
@ -170,6 +290,61 @@ export default {
this.service_node.key = key;
this.service_node.amount = minContribution;
},
minStake() {
const node = this.getNodeWithPubKey();
return this.getMinContribution(node);
},
maxStake() {
const node = this.getNodeWithPubKey();
return this.openForContributionLoki(node);
},
getFeeDecimal(node) {
const operatorPortion = node.portions_for_operator;
return (operatorPortion / 18446744073709551612) * 100;
},
getNodeWithPubKey() {
const key = this.service_node.key;
const nodeOfKey = this.awaiting_service_nodes.find(
n => n.service_node_pubkey === key
);
if (!nodeOfKey) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.invalidServiceNodeKey")
});
return;
} else {
return nodeOfKey;
}
},
onConfirmTransaction() {
// put the loading spinner up
this.$store.commit("gateway/set_sweep_all_status", {
code: DO_NOTHING,
message: "Getting sweep all tx information",
sending: true
});
const metadataList = this.confirmFields.metadataList;
const isBlink = this.confirmFields.isBlink;
const relayTxData = {
metadataList,
isBlink,
isSweepAll: true
};
// Commit the transaction
this.$gateway.send("wallet", "relay_tx", relayTxData);
},
onCancelTransaction() {
this.$store.commit("gateway/set_sweep_all_status", {
code: DO_NOTHING,
message: "Cancel the transaction from confirm dialog",
sending: false
});
},
sweepAllWarning() {
this.$q
.dialog({
@ -192,6 +367,16 @@ export default {
.onDismiss(() => {})
.onCancel(() => {});
},
buildDialogFieldsSweepAll(txData) {
this.confirmFields = this.buildDialogFields(txData);
},
areButtonsEnabled() {
// if we can find the service node key in the list of service nodes
const key = this.service_node.key;
return !!this.awaiting_service_nodes.find(
n => n.service_node_pubkey === key
);
},
async sweepAll() {
const { unlocked_balance } = this.info;
@ -214,12 +399,15 @@ export default {
passwordDialog
.onOk(password => {
password = password || "";
this.$store.commit("gateway/set_tx_status", {
code: 1,
this.$store.commit("gateway/set_sweep_all_status", {
code: DO_NOTHING,
message: "Sweeping all",
sending: true
});
const newTx = objectAssignDeep.noMutate(tx, { password });
const newTx = objectAssignDeep.noMutate(tx, {
password,
isSweepAll: true
});
this.$gateway.send("wallet", "transfer", newTx);
})
.onDismiss(() => {})
@ -272,7 +460,7 @@ export default {
noPasswordMessage: this.$t("dialog.stake.message"),
ok: {
label: this.$t("dialog.stake.ok"),
color: this.theme == "dark" ? "white" : "dark"
color: "primary"
},
dark: this.theme == "dark",
color: this.theme == "dark" ? "white" : "dark"

View File

@ -5,7 +5,9 @@
<span v-if="service_nodes.length">
{{ $t("titles.currentlyStakedNodes") }}
</span>
<span v-else>{{ $t("strings.serviceNodeStartStakingDescription") }}</span>
<span v-else>{{
$t("strings.serviceNodeStartStakingDescription")
}}</span>
</div>
<div v-if="service_nodes">
<ServiceNodeList
@ -15,10 +17,17 @@
:action="unlockWarning"
/>
</div>
<q-inner-loading :showing="unlock_status.sending || fetching" :dark="theme == 'dark'">
<q-inner-loading
:showing="unlock_status.sending || fetching"
:dark="theme == 'dark'"
>
<q-spinner color="primary" size="30" />
</q-inner-loading>
<ServiceNodeDetails ref="serviceNodeDetailsUnlock" :action="unlockWarning" action-i18n="buttons.unlock" />
<ServiceNodeDetails
ref="serviceNodeDetailsUnlock"
:action="unlockWarning"
action-i18n="buttons.unlock"
/>
</div>
</div>
</template>
@ -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) {

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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: {

View File

@ -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;
}
}
};

View File

@ -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);
}
}
};

View File

@ -1,7 +1,10 @@
<template>
<q-page class="create-wallet">
<div class="fields q-mx-md q-mt-md">
<LokiField :label="$t('fieldLabels.walletName')" :error="$v.wallet.name.$error">
<LokiField
:label="$t('fieldLabels.walletName')"
:error="$v.wallet.name.$error"
>
<q-input
v-model="wallet.name"
:dark="theme == 'dark'"
@ -48,7 +51,12 @@
/>
</LokiField>
<q-btn class="submit-button" color="primary" :label="$t('buttons.createWallet')" @click="create" />
<q-btn
class="submit-button"
color="primary"
:label="$t('buttons.createWallet')"
@click="create"
/>
</div>
</q-page>
</template>
@ -79,7 +87,7 @@ export default {
return {
wallet: {
name: "",
language: languageOptions[0],
language: languageOptions[0].value,
password: "",
password_confirm: ""
},

View File

@ -10,7 +10,10 @@
<div class="row gutter-md">
<!-- Amount -->
<div class="col-6 amount">
<LokiField :label="$t('fieldLabels.amount')" :error="$v.newTx.amount.$error">
<LokiField
:label="$t('fieldLabels.amount')"
:error="$v.newTx.amount.$error"
>
<q-input
v-model="newTx.amount"
:dark="theme == 'dark'"
@ -50,7 +53,10 @@
<!-- Address -->
<div class="col q-mt-sm">
<LokiField :label="$t('fieldLabels.address')" :error="$v.newTx.address.$error">
<LokiField
:label="$t('fieldLabels.address')"
:error="$v.newTx.address.$error"
>
<q-input
v-model.trim="newTx.address"
:dark="theme == 'dark'"
@ -59,7 +65,11 @@
dense
@blur="$v.newTx.address.$touch"
/>
<q-btn color="secondary" :text-color="theme == 'dark' ? 'white' : 'dark'" to="addressbook">
<q-btn
color="secondary"
:text-color="theme == 'dark' ? 'white' : 'dark'"
to="addressbook"
>
{{ $t("buttons.contacts") }}
</q-btn>
</LokiField>
@ -67,7 +77,11 @@
<!-- Payment ID -->
<div class="col q-mt-sm">
<LokiField :label="$t('fieldLabels.paymentId')" :error="$v.newTx.payment_id.$error" optional>
<LokiField
:label="$t('fieldLabels.paymentId')"
:error="$v.newTx.payment_id.$error"
optional
>
<!-- TODO: count to be '16 or 64 after RPC fixed -->
<q-input
v-model.trim="newTx.payment_id"
@ -139,40 +153,15 @@
/>
</div>
</div>
<q-dialog v-model="confirmTransaction" persistent>
<q-card class="confirm-tx-card" dark>
<q-card-section>
<div class="text-h6">{{ $t("dialog.confirmTransaction.title") }}</div>
</q-card-section>
<q-card-section>
<div class="confirm-list">
<div>
<span class="label">{{ $t("dialog.confirmTransaction.sendTo") }}: </span>
<br />
<span class="address-value">{{ confirmFields.destination }}</span>
</div>
<br />
<span class="label">{{ $t("strings.transactions.amount") }}: </span>
{{ confirmFields.totalAmount }} Loki
<br />
<span class="label">{{ $t("strings.transactions.fee") }}: </span> {{ confirmFields.totalFees }} Loki
<br />
<span class="label">{{ $t("dialog.confirmTransaction.priority") }}: </span>
{{ confirmFields.translatedBlinkOrSlow }}
</div>
</q-card-section>
<q-card-actions align="right">
<q-btn v-close-popup flat :label="$t('dialog.buttons.cancel')" color="negative" />
<q-btn
v-close-popup
class="confirm-send-btn"
flat
:label="$t('buttons.send')"
@click="onConfirmTransaction"
/>
</q-card-actions>
</q-card>
</q-dialog>
<ConfirmTransactionDialog
:show="confirmTransaction"
:amount="confirmFields.totalAmount"
:is-blink="confirmFields.isBlink"
:send-to="confirmFields.destination"
:fee="confirmFields.totalFees"
:on-confirm-transaction="onConfirmTransaction"
:on-cancel-transaction="onCancelTransaction"
/>
<q-inner-loading :showing="tx_status.sending" :dark="theme == 'dark'">
<q-spinner color="primary" size="30" />
</q-inner-loading>
@ -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 {
</script>
<style lang="scss">
.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;
}
}
.send {
.send-btn {
margin-top: 6px;

View File

@ -18,8 +18,14 @@ export const set_old_gui_import_status = (state, data) => {
export const set_tx_status = (state, data) => {
state.tx_status = data;
};
export const set_sweep_all_status = (state, data) => {
state.sweep_all_status = data;
};
export const set_snode_status = (state, data) => {
state.service_node_status = objectAssignDeep.noMutate(state.service_node_status, data);
state.service_node_status = objectAssignDeep.noMutate(
state.service_node_status,
data
);
};
export const set_prove_transaction_status = (state, data) => {
state.prove_transaction_status = {

View File

@ -57,6 +57,13 @@ export default {
i18n: "",
sending: false
},
// differentiate between a tx and sweep_all
sweep_all_status: {
code: 0,
message: "",
i18n: "",
sending: false
},
service_node_status: {
stake: {
code: 0,