Merge pull request #122 from Mikunj/prettier

Improve Linting
This commit is contained in:
Mikunj Varsani 2020-03-04 15:13:42 +11:00 committed by GitHub
commit 9887813f60
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
95 changed files with 15291 additions and 13975 deletions

View File

@ -2,8 +2,6 @@ root = true
[*]
charset = utf-8
indent_style = space
indent_size = 4
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

View File

@ -1,52 +1,24 @@
module.exports = {
root: true,
parserOptions: {
parser: "babel-eslint",
sourceType: "module"
},
env: {
browser: true
},
extends: [
// https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention
// consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules.
"plugin:vue/essential",
// https://github.com/standard/standard/blob/master/docs/RULES-en.md
"standard"
],
// required to lint *.vue files
plugins: [
"vue"
],
globals: {
"ga": true, // Google Analytics
"cordova": true,
"__statics": true
},
// add your custom rules here
"rules": {
"indent": ["error", 4],
"quotes": ["error", "double"],
// allow async-await
"generator-star-spacing": "off",
// allow paren-less arrow functions
"arrow-parens": 0,
"one-var": 0,
"camelcase": 0,
"import/first": 0,
"import/named": 2,
"import/namespace": 2,
"import/default": 2,
"import/export": 2,
"import/extensions": 0,
"import/no-unresolved": 0,
"import/no-extraneous-dependencies": 0,
// allow debugger during development
"no-debugger": process.env.NODE_ENV === "production" ? 2 : 0
}
}
root: true,
parserOptions: {
parser: "babel-eslint"
},
env: {
node: true,
browser: true
},
extends: ["plugin:prettier/recommended", "plugin:vue/recommended", "eslint:recommended", "prettier/vue"],
// required to lint *.vue files
plugins: ["vue"],
globals: {
__statics: true,
__ryo_bin: true
},
// add your custom rules here
rules: {
// allow debugger during development
"no-console": process.env.NODE_ENV === "production" ? "error" : "off",
"no-debugger": process.env.NODE_ENV === "production" ? "error" : "off",
"vue/component-name-in-template-casing": ["error", "PascalCase"]
}
};

View File

@ -1,13 +1,11 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
title: ""
labels: bug
assignees: ''
---
**Describe the bug**
assignees: ""
---**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
@ -17,5 +15,6 @@ Steps to reproduce the behavior:
If applicable, add screenshots or Log files to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. Ubuntu 16.04, Windows 10]
- Version of Wallet or Git commit hash
- OS: [e.g. Ubuntu 16.04, Windows 10]
- Version of Wallet or Git commit hash

View File

@ -1,79 +1,78 @@
name: Loki Electron Wallet Build
on:
push:
branches:
- master
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
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
- name: Install node
uses: actions/setup-node@v1
with:
node-version: "11.9.0"
- name: Install node
uses: actions/setup-node@v1
with:
node-version: "11.9.0"
- name: Install dependencies
run: npm install
- name: Install dependencies
run: npm install
- name: Download lokid binaries
run: node ./build/download-binaries.js
- name: Download lokid binaries
run: node ./build/download-binaries.js
- name: Extract zip binaries
if: runner.os != 'Linux'
run: unzip latest.zip
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: 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: 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: 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 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 }}
- 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 }}
- name: Remove un-needed artifacts
run: rm -r -- ./*/
shell: bash
working-directory: ./dist/electron-mat/Packaged
- name: Remove un-needed artifacts
run: rm -r -- ./*/
shell: bash
working-directory: ./dist/electron-mat/Packaged
- name: Upload artifacts
uses: actions/upload-artifact@v1
with:
name: ${{ runner.OS }}
path: dist/electron-mat/Packaged
- name: Upload artifacts
uses: actions/upload-artifact@v1
with:
name: ${{ runner.OS }}
path: dist/electron-mat/Packaged

View File

@ -1,8 +1,8 @@
// https://github.com/michael-ciniawsky/postcss-load-config
module.exports = {
plugins: [
// to edit target browsers: use "browserslist" field in package.json
require("autoprefixer")
]
}
plugins: [
// to edit target browsers: use "browserslist" field in package.json
require("autoprefixer")
]
};

33
.prettierignore Normal file
View File

@ -0,0 +1,33 @@
.quasar
.DS_Store
.thumbs.db
node_modules
/dist
/src-cordova/platforms
/src-cordova/plugins
/src-cordova/www
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*~
\#*\#
.\#*
*.bak
# bin dir
bin/*
!bin/.gitkeep
.env
/downloads
/src/validators/address_tools.js

3
.prettierrc.js Normal file
View File

@ -0,0 +1,3 @@
module.exports = {
printWidth: 120
};

View File

@ -26,17 +26,17 @@ base64 -i certificate.p12 -o encoded.txt
3. In the left sidebar, click **Secrets**.
4. Add the following secrets:
1. Certificate
* Name: `MAC_CERTIFICATE`
* Value: The encoded Base64 certificate
- Name: `MAC_CERTIFICATE`
- Value: The encoded Base64 certificate
2. Certificate password
* Name: `MAC_CERTIFICATE_PASSWORD`
* Value: The password that was set when the certificate was exported.
- Name: `MAC_CERTIFICATE_PASSWORD`
- Value: The password that was set when the certificate was exported.
3. Apple ID
* Name: `SIGNING_APPLE_ID`
* Value: The apple id (email) to use for signing
- Name: `SIGNING_APPLE_ID`
- Value: The apple id (email) to use for signing
4. Apple Password
* Name: `SIGNING_APP_PASSWORD`
* Value: The app-specific password that was generated for the apple id
- Name: `SIGNING_APP_PASSWORD`
- Value: The app-specific password that was generated for the apple id
5. Team ID (Optional)
* Name: `SIGNING_TEAM_ID`
* Value: The apple team id if you're sigining the application for a team
- Name: `SIGNING_TEAM_ID`
- Value: The apple team id if you're sigining the application for a team

View File

@ -1,14 +1,14 @@
# Loki Electron GUI Wallet
### Introduction
Loki is a private cryptocurrency based on Monero. Loki aims to provide a private data transmission layer using a second layer of Service Nodes.
More information on the project can be found on the [website](https://loki.network) and in the [whitepaper](https://loki.network/whitepaper). Loki is an open source project, and we encourage contributions from anyone with something to offer.
<p align="center">
<img src="https://raw.githubusercontent.com/KeeJef/loki-electron-gui-wallet/master/src-electron/icons/mrcuug.PNG" width="600">
</p>
### About this project
This is the new electron GUI for Loki. It is open source and completely free to use without restrictions, anyone may create an alternative implementation of the Loki Electron GUI that uses the protocol and network in a compatible manner.
@ -16,9 +16,11 @@ This is the new electron GUI for Loki. It is open source and completely free to
Please submit any changes as pull requests to the development branch, all changes are assessed in the development branch before being merged to master, release tags are considered stable builds for the GUI.
#### Pre-requisites
- Download latest [Lokid](https://github.com/loki-project/loki/releases/latest)
#### Commands
```
nvm use 11.9.0
npm install -g quasar-cli
@ -30,6 +32,7 @@ npm install
```
For dev:
```
npm run dev
```
@ -37,6 +40,7 @@ npm run dev
For building:
**Note:** This will only build the binaries for the system you run the command on. Running this command on `linux` will only make `linux` binaries, no `mac` or `windows` binaries.
```
npm run build
```
@ -47,14 +51,14 @@ Adding a new language is fairly simple.
1. Duplicate the language file `src/i18n/en-us.js` and rename it to the relevant language code.
2. Translate all the strings in that duplicated file. Take note that capitalization matters.
- The translated string must go in-between the quotes (`""`)
- E.G `all: "ALL"` -> `all: "ВСЕ"`
- If possible try and stick to the general string formatting already present.
- E.G if there is a new line then try and keep that in your translation.
- The same goes for the pipe character `|`. **DO NOT REMOVE IT**.
- Please don't translate strings inside `{}` brackets. They are meant as placeholders for other values.
- Some examples include `{type}` and `{count}`.
- E.G if you have a string `A {index}` then you may translate it as `B {index}` or `{index} B` depending on how the string makes sense in your language. You are allowed to reposition the placeholders for the string to make sense **BUT DO NOT DELETE OR REPLACE THE PLACEHOLDERS WITH OTHER VALUES**
- The translated string must go in-between the quotes (`""`)
- E.G `all: "ALL"` -> `all: "ВСЕ"`
- If possible try and stick to the general string formatting already present.
- E.G if there is a new line then try and keep that in your translation.
- The same goes for the pipe character `|`. **DO NOT REMOVE IT**.
- Please don't translate strings inside `{}` brackets. They are meant as placeholders for other values.
- Some examples include `{type}` and `{count}`.
- E.G if you have a string `A {index}` then you may translate it as `B {index}` or `{index} B` depending on how the string makes sense in your language. You are allowed to reposition the placeholders for the string to make sense **BUT DO NOT DELETE OR REPLACE THE PLACEHOLDERS WITH OTHER VALUES**
3. Add the language to the `languages` array in `src/i18n/index.js`. The `flag` property is the [ISO 3166-1-alpha-2 code](https://www.iso.org/obp/ui/#search/code/) of a country.
- **NOTE: DO NOT ADD THE LANGUAGE TO `export default`**. Dynamic language loading is handled by the application.
4. Add your language locale to Vue Timeago. Add it in `src/plugins/timeago.js` under `locales`.

View File

@ -9,5 +9,3 @@ Releasing the application to work with auto update is very simple.
- E.g if the version was `2.1.1` then the github tag would be `v2.1.1`
4. Add release notes
5. Publish the release!

View File

@ -1,42 +1,46 @@
const axios = require("axios").default
const fs = require("fs-extra")
const path = require("path")
const axios = require("axios").default;
const fs = require("fs-extra");
const path = require("path");
async function download () {
const { platform } = process
const repoUrl = "https://api.github.com/repos/loki-project/loki/releases/latest"
try {
const pwd = process.cwd()
const downloadDir = path.join(pwd, "downloads")
await fs.ensureDir(downloadDir)
async function download() {
const { platform } = process;
const repoUrl = "https://api.github.com/repos/loki-project/loki/releases/latest";
try {
const pwd = process.cwd();
const downloadDir = path.join(pwd, "downloads");
await fs.ensureDir(downloadDir);
const { data } = await axios.get(repoUrl)
const { name } = data
console.log("Latest release: " + name)
const { data } = await axios.get(repoUrl);
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")
})
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)
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()
download();

View File

@ -1,5 +1,5 @@
require("dotenv").config()
const { notarize } = require("electron-notarize")
require("dotenv").config();
const { notarize } = require("electron-notarize");
/*
Pre-requisites: https://github.com/electron/electron-notarize#prerequisites
@ -11,36 +11,30 @@ const { notarize } = require("electron-notarize")
Notarizing: https://kilianvalkhof.com/2019/electron/notarizing-your-electron-application/
*/
const log = msg => console.log(`\n${msg}`)
const isEmpty = v => !v || v.length === 0
const log = msg => console.log(`\n${msg}`);
const isEmpty = v => !v || v.length === 0;
exports.default = async function notarizing (context) {
const { electronPlatformName, appOutDir } = context
if (electronPlatformName !== "darwin") {
return
}
log("Notarizing mac application")
exports.default = async function notarizing(context) {
const { electronPlatformName, appOutDir } = context;
if (electronPlatformName !== "darwin") {
return;
}
log("Notarizing mac application");
const appName = context.packager.appInfo.productFilename
const {
SIGNING_APPLE_ID,
SIGNING_APP_PASSWORD,
SIGNING_TEAM_ID
} = process.env
const appName = context.packager.appInfo.productFilename;
const { SIGNING_APPLE_ID, SIGNING_APP_PASSWORD, SIGNING_TEAM_ID } = process.env;
if (isEmpty(SIGNING_APPLE_ID) || isEmpty(SIGNING_APP_PASSWORD)) {
log(
"SIGNING_APPLE_ID or SIGNING_APP_PASSWORD not set.\nTerminating noratization."
)
return
}
if (isEmpty(SIGNING_APPLE_ID) || isEmpty(SIGNING_APP_PASSWORD)) {
log("SIGNING_APPLE_ID or SIGNING_APP_PASSWORD not set.\nTerminating noratization.");
return;
}
const options = {
appBundleId: "com.loki-project.electron-wallet",
appPath: `${appOutDir}/${appName}.app`,
appleId: SIGNING_APPLE_ID,
appleIdPassword: SIGNING_APP_PASSWORD
}
if (!isEmpty(SIGNING_TEAM_ID)) options.ascProvider = SIGNING_TEAM_ID
return notarize(options)
}
const options = {
appBundleId: "com.loki-project.electron-wallet",
appPath: `${appOutDir}/${appName}.app`,
appleId: SIGNING_APPLE_ID,
appleIdPassword: SIGNING_APP_PASSWORD
};
if (!isEmpty(SIGNING_TEAM_ID)) options.ascProvider = SIGNING_TEAM_ID;
return notarize(options);
};

1413
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -17,9 +17,9 @@
"dev": "quasar dev -m electron -t mat",
"build": "quasar build -m electron -t mat",
"release": "quasar build -m electron -t mat --publish=always",
"lint": "eslint --ext .js,.vue src",
"lint-fix": "eslint --fix .",
"test": "echo \"No test specified\" && exit 0"
"lint": "eslint --fix .",
"format": "prettier --write \"**/*.+(js|jsx|json|yml|yaml|css|md|vue)\"",
"ready": "npm run lint && npm run format"
},
"dependencies": {
"axios": "^0.18.1",
@ -41,7 +41,7 @@
"vuelidate": "^0.7.4"
},
"devDependencies": {
"babel-eslint": "^10.0.1",
"babel-eslint": "^10.1.0",
"devtron": "^1.4.0",
"dotenv": "^8.1.0",
"electron": "^4.1.1",
@ -50,20 +50,35 @@
"electron-devtools-installer": "^2.2.4",
"electron-notarize": "^0.1.1",
"electron-packager": "^13.1.1",
"eslint": "^5.15.3",
"eslint-config-standard": "^12.0.0",
"eslint": "^5.16.0",
"eslint-config-prettier": "^6.10.0",
"eslint-friendly-formatter": "^4.0.1",
"eslint-loader": "^2.1.2",
"eslint-loader": "^2.2.1",
"eslint-plugin-import": "^2.16.0",
"eslint-plugin-node": "^8.0.1",
"eslint-plugin-prettier": "^3.1.2",
"eslint-plugin-promise": "^4.0.1",
"eslint-plugin-standard": "^4.0.0",
"eslint-plugin-vue": "^5.2.2",
"node-sass": "^4.11.0",
"eslint-plugin-vue": "^5.2.3",
"husky": "^4.2.3",
"lint-staged": "^10.0.8",
"node-sass": "^4.13.1",
"prettier": "^1.19.1",
"quasar-cli": "^0.17.26",
"sass-loader": "^7.1.0",
"strip-ansi": "^3.0.1"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.+(js|jsx|vue)": [
"eslint --fix",
"prettier --write"
],
"*.+(json|yml|yaml|css|md)": "prettier --write"
},
"engines": {
"node": ">= 8.9.0",
"npm": ">= 5.6.0",

View File

@ -1,37 +1,28 @@
/* eslint-disable no-template-curly-in-string */
// Configuration for your app
module.exports = function (ctx) {
return {
// app plugins (/src/plugins)
plugins: [
"i18n",
"axios",
"vuelidate",
"gateway",
"timeago"
],
css: [
"app.styl",
"~flag-icon-css/css/flag-icon.min.css"
],
extras: [
ctx.theme.mat ? "roboto-font" : null,
"material-icons" // optional, you are not bound to it
// "ionicons",
// "mdi",
// "fontawesome"
],
supportIE: false,
build: {
scopeHoisting: true,
vueRouterMode: "history",
// vueCompiler: true,
// gzip: true,
// analyze: true,
// extractCSS: false,
extendWebpack (cfg) {
/*
module.exports = function(ctx) {
return {
// app plugins (/src/plugins)
plugins: ["i18n", "axios", "vuelidate", "gateway", "timeago"],
css: ["app.styl", "~flag-icon-css/css/flag-icon.min.css"],
extras: [
ctx.theme.mat ? "roboto-font" : null,
"material-icons" // optional, you are not bound to it
// "ionicons",
// "mdi",
// "fontawesome"
],
supportIE: false,
build: {
scopeHoisting: true,
vueRouterMode: "history",
// vueCompiler: true,
// gzip: true,
// analyze: true,
// extractCSS: false,
extendWebpack() {
/*
cfg.module.rules.push({
enforce: "pre",
test: /\.(js|vue)$/,
@ -39,187 +30,173 @@ module.exports = function (ctx) {
exclude: /(node_modules|quasar)/
})
*/
}
}
},
devServer: {
// https: true,
// port: 8080,
open: true // opens browser window automatically
},
// framework: "all" --- includes everything; for dev only!
framework: {
components: [
"QLayout",
"QLayoutHeader",
"QLayoutFooter",
"QLayoutDrawer",
"QPageContainer",
"QPage",
"QToolbar",
"QToolbarTitle",
"QTooltip",
"QField",
"QInput",
"QRadio",
"QOptionGroup",
"QBtn",
"QBtnToggle",
"QIcon",
"QTabs",
"QTab",
"QRouteTab",
"QBtnDropdown",
"QPopover",
"QModal",
"QModalLayout",
"QStep",
"QStepper",
"QStepperNavigation",
"QSpinner",
"QList",
"QListHeader",
"QItem",
"QItemMain",
"QItemSeparator",
"QItemSide",
"QItemTile",
"QSelect",
"QToggle",
"QPageSticky",
"QCollapsible",
"QCheckbox",
"QInnerLoading",
"QInfiniteScroll",
"QDatetime",
"QContextMenu",
"QScrollArea"
],
directives: ["Ripple", "CloseOverlay"],
// Quasar plugins
plugins: ["Notify", "Loading", "LocalStorage", "Dialog"]
// iconSet: ctx.theme.mat ? "material-icons" : "ionicons"
// i18n: "de" // Quasar language
},
// animations: "all" --- includes all animations
animations: [],
pwa: {
// workboxPluginMode: "InjectManifest",
// workboxOptions: {},
manifest: {
// name: "Quasar App",
// short_name: "Quasar-PWA",
// description: "Best PWA App in town!",
display: "standalone",
orientation: "portrait",
background_color: "#ffffff",
theme_color: "#43BD43",
icons: [
{
src: "statics/icons/icon-128x128.png",
sizes: "128x128",
type: "image/png"
},
{
src: "statics/icons/icon-192x192.png",
sizes: "192x192",
type: "image/png"
},
{
src: "statics/icons/icon-256x256.png",
sizes: "256x256",
type: "image/png"
},
{
src: "statics/icons/icon-384x384.png",
sizes: "384x384",
type: "image/png"
},
{
src: "statics/icons/icon-512x512.png",
sizes: "512x512",
type: "image/png"
}
]
}
},
cordova: {
// id: "org.cordova.quasar.app"
},
electron: {
bundler: "builder", // or "packager"
extendWebpack() {
// cfg
// do something with Electron process Webpack cfg
},
packager: {
// https://github.com/electron-userland/electron-packager/blob/master/docs/api.md#options
// OS X / Mac App Store
// appBundleId: "",
// appCategoryType: "",
// osxSign: "",
// protocol: "myapp://path",
// Window only
// win32metadata: { ... }
extraResource: ["bin"]
},
builder: {
// https://www.electron.build/configuration/configuration
appId: "com.loki-project.electron-wallet",
productName: "Loki Electron Wallet",
copyright: "Copyright © 2018-2019 Loki Project, 2018 Ryo Currency Project",
afterSign: "build/notarize.js",
artifactName: "loki-electron-wallet-${version}-${os}.${ext}",
publish: "github",
linux: {
target: ["AppImage"],
icon: "src-electron/icons/icon_512x512.png",
category: "Finance"
},
devServer: {
// https: true,
// port: 8080,
open: true // opens browser window automatically
mac: {
target: ["dmg"],
icon: "src-electron/icons/icon.icns",
category: "public.app-category.finance",
// Notarizing: https://kilianvalkhof.com/2019/electron/notarizing-your-electron-application/
hardenedRuntime: true,
gatekeeperAssess: false,
entitlements: "build/entitlements.mac.plist",
entitlementsInherit: "build/entitlements.mac.plist"
},
// framework: "all" --- includes everything; for dev only!
framework: {
components: [
"QLayout",
"QLayoutHeader",
"QLayoutFooter",
"QLayoutDrawer",
"QPageContainer",
"QPage",
"QToolbar",
"QToolbarTitle",
"QTooltip",
"QField",
"QInput",
"QRadio",
"QOptionGroup",
"QBtn",
"QBtnToggle",
"QIcon",
"QTabs",
"QTab",
"QRouteTab",
"QBtnDropdown",
"QPopover",
"QModal",
"QModalLayout",
"QStep",
"QStepper",
"QStepperNavigation",
"QSpinner",
"QList",
"QListHeader",
"QItem",
"QItemMain",
"QItemSeparator",
"QItemSide",
"QItemTile",
"QSelect",
"QToggle",
"QPageSticky",
"QCollapsible",
"QCheckbox",
"QInnerLoading",
"QInfiniteScroll",
"QDatetime",
"QContextMenu",
"QScrollArea"
],
directives: [
"Ripple",
"CloseOverlay"
],
// Quasar plugins
plugins: [
"Notify",
"Loading",
"LocalStorage",
"Dialog"
]
// iconSet: ctx.theme.mat ? "material-icons" : "ionicons"
// i18n: "de" // Quasar language
dmg: {
background: "src-electron/build/loki-dmg.tiff",
sign: false
},
// animations: "all" --- includes all animations
animations: [],
pwa: {
// workboxPluginMode: "InjectManifest",
// workboxOptions: {},
manifest: {
// name: "Quasar App",
// short_name: "Quasar-PWA",
// description: "Best PWA App in town!",
display: "standalone",
orientation: "portrait",
background_color: "#ffffff",
theme_color: "#43BD43",
icons: [{
"src": "statics/icons/icon-128x128.png",
"sizes": "128x128",
"type": "image/png"
},
{
"src": "statics/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "statics/icons/icon-256x256.png",
"sizes": "256x256",
"type": "image/png"
},
{
"src": "statics/icons/icon-384x384.png",
"sizes": "384x384",
"type": "image/png"
},
{
"src": "statics/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}
nsis: {
oneClick: false,
allowToChangeInstallationDirectory: true
},
cordova: {
// id: "org.cordova.quasar.app"
},
electron: {
bundler: "builder", // or "packager"
extendWebpack (cfg) {
// do something with Electron process Webpack cfg
},
packager: {
// https://github.com/electron-userland/electron-packager/blob/master/docs/api.md#options
// OS X / Mac App Store
// appBundleId: "",
// appCategoryType: "",
// osxSign: "",
// protocol: "myapp://path",
files: ["!build/*.js", "!.env", "!dev-app-update.yml"],
// Window only
// win32metadata: { ... }
extraResource: [
"bin"
]
},
builder: {
// https://www.electron.build/configuration/configuration
appId: "com.loki-project.electron-wallet",
productName: "Loki Electron Wallet",
copyright: "Copyright © 2018-2019 Loki Project, 2018 Ryo Currency Project",
afterSign: "build/notarize.js",
artifactName: "loki-electron-wallet-${version}-${os}.${ext}",
publish: "github",
linux: {
target: ["AppImage"],
icon: "src-electron/icons/icon_512x512.png",
category: "Finance"
},
mac: {
target: ["dmg"],
icon: "src-electron/icons/icon.icns",
category: "public.app-category.finance",
// Notarizing: https://kilianvalkhof.com/2019/electron/notarizing-your-electron-application/
hardenedRuntime: true,
gatekeeperAssess: false,
entitlements: "build/entitlements.mac.plist",
entitlementsInherit: "build/entitlements.mac.plist"
},
dmg: {
background: "src-electron/build/loki-dmg.tiff",
sign: false
},
nsis: {
oneClick: false,
allowToChangeInstallationDirectory: true
},
files: [
"!build/*.js",
"!.env",
"!dev-app-update.yml"
],
extraResources: [
"bin"
]
}
}
extraResources: ["bin"]
}
}
}
};
};

View File

@ -1,86 +1,89 @@
import { dialog } from "electron"
import isDev from "electron-is-dev"
import { autoUpdater } from "electron-updater"
import ProgressBar from "electron-progressbar"
import { dialog } from "electron";
import isDev from "electron-is-dev";
import { autoUpdater } from "electron-updater";
import ProgressBar from "electron-progressbar";
let progressBar = null
let isUpdating = false
let downloadAndInstall = false
let progressBar = null;
let isUpdating = false;
let downloadAndInstall = false;
function checkForUpdate (onQuitAndInstall) {
// Disable for development
if (isDev) {
return
function checkForUpdate(onQuitAndInstall) {
// Disable for development
if (isDev) {
return;
}
autoUpdater.logger = console;
autoUpdater.autoDownload = false;
autoUpdater.on("error", err => {
if (isUpdating) {
dialog.showErrorBox("Update Error: ", err == null ? "unknown" : err.message);
isUpdating = false;
console.error("Error in auto-updater.", err.message);
}
});
autoUpdater.on("update-available", info => {
console.log(`Update available: ${info.version}`);
const message = `Update ${info.version} found. Do you want to download the update?`;
const detail = `View the release notes at: https://github.com/loki-project/loki-electron-gui-wallet/releases/tag/v${info.version}`;
dialog.showMessageBox(
{
type: "info",
title: "Update available",
message,
detail,
buttons: ["Download and Install", "Download and Install Later", "No"],
defaultId: 0
},
buttonIndex => {
// Download and install
if (buttonIndex === 0) {
downloadAndInstall = true;
if (!progressBar) {
progressBar = new ProgressBar({
indeterminate: false,
title: "Downloading...",
text: `Downloading wallet v${info.version}`
});
}
}
// Download
if (buttonIndex !== 2) {
isUpdating = true;
autoUpdater.downloadUpdate();
}
}
);
});
autoUpdater.on("download-progress", progress => {
if (progressBar) {
progressBar.value = progress.percent;
}
});
autoUpdater.on("update-downloaded", () => {
console.log("Update downloaded");
isUpdating = false;
if (progressBar) {
progressBar.setCompleted();
progressBar = null;
}
autoUpdater.logger = console
autoUpdater.autoDownload = false
// If download and install was selected then quit and install
if (downloadAndInstall && onQuitAndInstall) {
onQuitAndInstall(autoUpdater);
downloadAndInstall = false;
}
});
autoUpdater.on("error", (err) => {
if (isUpdating) {
dialog.showErrorBox("Update Error: ", err == null ? "unknown" : err.message)
isUpdating = false
console.error("Error in auto-updater.", err.message)
}
})
autoUpdater.on("update-available", info => {
console.log(`Update available: ${info.version}`)
const message = `Update ${info.version} found. Do you want to download the update?`
const detail = `View the release notes at: https://github.com/loki-project/loki-electron-gui-wallet/releases/tag/v${info.version}`
dialog.showMessageBox({
type: "info",
title: "Update available",
message,
detail,
buttons: ["Download and Install", "Download and Install Later", "No"],
defaultId: 0
}, (buttonIndex) => {
// Download and install
if (buttonIndex === 0) {
downloadAndInstall = true
if (!progressBar) {
progressBar = new ProgressBar({
indeterminate: false,
title: "Downloading...",
text: `Downloading wallet v${info.version}`
})
}
}
// Download
if (buttonIndex !== 2) {
isUpdating = true
autoUpdater.downloadUpdate()
}
})
})
autoUpdater.on("download-progress", progress => {
if (progressBar) {
progressBar.value = progress.percent
}
})
autoUpdater.on("update-downloaded", () => {
console.log("Update downloaded")
isUpdating = false
if (progressBar) {
progressBar.setCompleted()
progressBar = null
}
// If download and install was selected then quit and install
if (downloadAndInstall && onQuitAndInstall) {
onQuitAndInstall(autoUpdater)
downloadAndInstall = false
}
})
autoUpdater.checkForUpdates()
autoUpdater.checkForUpdates();
}
export { checkForUpdate }
export { checkForUpdate };

View File

@ -7,18 +7,19 @@
// Install `electron-debug` with `devtron`
require("electron-debug")({
showDevTools: true
})
showDevTools: true
});
// Install `vue-devtools`
require("electron").app.on("ready", () => {
let installExtension = require("electron-devtools-installer")
installExtension.default(installExtension.VUEJS_DEVTOOLS)
.then(() => {})
.catch(err => {
console.log("Unable to install `vue-devtools`: \n", err)
})
})
let installExtension = require("electron-devtools-installer");
installExtension
.default(installExtension.VUEJS_DEVTOOLS)
.then(() => {})
.catch(err => {
console.log("Unable to install `vue-devtools`: \n", err);
});
});
// Require `main` process to boot app
require("./electron-main")
require("./electron-main");

View File

@ -1,208 +1,208 @@
import { app, ipcMain, BrowserWindow, Menu, dialog } from "electron"
import { version, productName } from "../../package.json"
import { Backend } from "./modules/backend"
import { checkForUpdate } from "./auto-updater"
import menuTemplate from "./menu"
import isDev from "electron-is-dev"
const portscanner = require("portscanner")
const windowStateKeeper = require("electron-window-state")
const path = require("upath")
import { app, ipcMain, BrowserWindow, Menu, dialog } from "electron";
import { version, productName } from "../../package.json";
import { Backend } from "./modules/backend";
import { checkForUpdate } from "./auto-updater";
import menuTemplate from "./menu";
import isDev from "electron-is-dev";
const portscanner = require("portscanner");
const windowStateKeeper = require("electron-window-state");
const path = require("upath");
/**
* Set `__statics` path to static files in production;
* The reason we are setting it here is that the path needs to be evaluated at runtime
*/
if (process.env.PROD) {
global.__statics = path.join(__dirname, "statics").replace(/\\/g, "\\\\")
global.__ryo_bin = path.join(__dirname, "..", "bin").replace(/\\/g, "\\\\")
global.__statics = path.join(__dirname, "statics").replace(/\\/g, "\\\\");
global.__ryo_bin = path.join(__dirname, "..", "bin").replace(/\\/g, "\\\\");
} else {
global.__ryo_bin = path.join(process.cwd(), "bin").replace(/\\/g, "\\\\")
global.__ryo_bin = path.join(process.cwd(), "bin").replace(/\\/g, "\\\\");
}
let mainWindow, backend
let showConfirmClose = true
let forceQuit = false
let installUpdate = false
let mainWindow, backend;
let showConfirmClose = true;
let forceQuit = false;
let installUpdate = false;
const title = `${productName} v${version}`
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" },
{ role: "copy" },
{ role: "paste" },
{ type: "separator" },
{ role: "selectall" }
])
{ role: "cut" },
{ role: "copy" },
{ role: "paste" },
{ type: "separator" },
{ role: "selectall" }
]);
function createWindow () {
/**
* Initial window options
*/
function createWindow() {
/**
* Initial window options
*/
let mainWindowState = windowStateKeeper({
defaultWidth: 900,
defaultHeight: 700
})
let mainWindowState = windowStateKeeper({
defaultWidth: 900,
defaultHeight: 700
});
mainWindow = new BrowserWindow({
x: mainWindowState.x,
y: mainWindowState.y,
width: mainWindowState.width,
height: mainWindowState.height,
minWidth: 640,
minHeight: 480,
icon: require("path").join(__statics, "icon_512x512.png"),
title
})
mainWindow = new BrowserWindow({
x: mainWindowState.x,
y: mainWindowState.y,
width: mainWindowState.width,
height: mainWindowState.height,
minWidth: 640,
minHeight: 480,
icon: require("path").join(__statics, "icon_512x512.png"),
title
});
mainWindow.on("close", (e) => {
// Don't ask for confirmation if we're installing an update
if (installUpdate) { return }
mainWindow.on("close", e => {
// Don't ask for confirmation if we're installing an update
if (installUpdate) {
return;
}
if (process.platform === "darwin") {
if (forceQuit) {
forceQuit = false
if (showConfirmClose) {
e.preventDefault()
mainWindow.show()
mainWindow.webContents.send("confirmClose")
} else {
e.defaultPrevented = false
}
} else {
e.preventDefault()
mainWindow.hide()
}
if (process.platform === "darwin") {
if (forceQuit) {
forceQuit = false;
if (showConfirmClose) {
e.preventDefault();
mainWindow.show();
mainWindow.webContents.send("confirmClose");
} else {
if (showConfirmClose) {
e.preventDefault()
mainWindow.webContents.send("confirmClose")
} else {
e.defaultPrevented = false
}
e.defaultPrevented = false;
}
})
} else {
e.preventDefault();
mainWindow.hide();
}
} else {
if (showConfirmClose) {
e.preventDefault();
mainWindow.webContents.send("confirmClose");
} else {
e.defaultPrevented = false;
}
}
});
ipcMain.on("confirmClose", (e, restart) => {
showConfirmClose = false
ipcMain.on("confirmClose", (e, restart) => {
showConfirmClose = false;
// In dev mode, this will launch a blank white screen
if (restart && !isDev) app.relaunch()
// In dev mode, this will launch a blank white screen
if (restart && !isDev) app.relaunch();
const promise = backend ? backend.quit() : Promise.resolve()
promise.then(() => {
backend = null
app.quit()
})
})
const promise = backend ? backend.quit() : Promise.resolve();
promise.then(() => {
backend = null;
app.quit();
});
});
mainWindow.webContents.on("did-finish-load", () => {
// Set the title
mainWindow.setTitle(title)
mainWindow.webContents.on("did-finish-load", () => {
// Set the title
mainWindow.setTitle(title);
require("crypto").randomBytes(64, (err, buffer) => {
// if err, then we may have to use insecure token generation perhaps
if (err) throw err
require("crypto").randomBytes(64, (err, buffer) => {
// if err, then we may have to use insecure token generation perhaps
if (err) throw err;
let config = {
port: 12313,
token: buffer.toString("hex")
}
let config = {
port: 12313,
token: buffer.toString("hex")
};
portscanner.checkPortStatus(config.port, "127.0.0.1", (error, status) => {
if (error) {
console.error(error)
}
if (status === "closed") {
backend = new Backend(mainWindow)
backend.init(config)
mainWindow.webContents.send("initialize", config)
} else {
dialog.showMessageBox(mainWindow, {
title: "Startup error",
message: `Loki Wallet is already open, or port ${config.port} is in use`,
type: "error",
buttons: ["ok"]
}, () => {
showConfirmClose = false
app.quit()
})
}
})
})
})
mainWindow.webContents.on("context-menu", (e, props) => {
const { selectionText, isEditable } = props
if (isEditable) {
inputMenu.popup(mainWindow)
} else if (selectionText && selectionText.trim() !== "") {
selectionMenu.popup(mainWindow)
portscanner.checkPortStatus(config.port, "127.0.0.1", (error, status) => {
if (error) {
console.error(error);
}
})
mainWindow.loadURL(process.env.APP_URL)
mainWindowState.manage(mainWindow)
if (status === "closed") {
backend = new Backend(mainWindow);
backend.init(config);
mainWindow.webContents.send("initialize", config);
} else {
dialog.showMessageBox(
mainWindow,
{
title: "Startup error",
message: `Loki Wallet is already open, or port ${config.port} is in use`,
type: "error",
buttons: ["ok"]
},
() => {
showConfirmClose = false;
app.quit();
}
);
}
});
});
});
mainWindow.webContents.on("context-menu", (e, props) => {
const { selectionText, isEditable } = props;
if (isEditable) {
inputMenu.popup(mainWindow);
} else if (selectionText && selectionText.trim() !== "") {
selectionMenu.popup(mainWindow);
}
});
mainWindow.loadURL(process.env.APP_URL);
mainWindowState.manage(mainWindow);
}
app.on("ready", () => {
checkForUpdate(autoUpdater => {
if (mainWindow) {
mainWindow.webContents.send("showQuitScreen")
}
const promise = backend ? backend.quit() : Promise.resolve()
promise.then(() => {
installUpdate = true
backend = null
autoUpdater.quitAndInstall()
})
})
if (process.platform === "darwin") {
const menu = Menu.buildFromTemplate(menuTemplate)
Menu.setApplicationMenu(menu)
checkForUpdate(autoUpdater => {
if (mainWindow) {
mainWindow.webContents.send("showQuitScreen");
}
createWindow()
})
const promise = backend ? backend.quit() : Promise.resolve();
promise.then(() => {
installUpdate = true;
backend = null;
autoUpdater.quitAndInstall();
});
});
if (process.platform === "darwin") {
const menu = Menu.buildFromTemplate(menuTemplate);
Menu.setApplicationMenu(menu);
}
createWindow();
});
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
app.quit()
}
})
if (process.platform !== "darwin") {
app.quit();
}
});
app.on("activate", () => {
if (mainWindow === null) {
createWindow()
} else if (process.platform === "darwin") {
mainWindow.show()
}
})
if (mainWindow === null) {
createWindow();
} else if (process.platform === "darwin") {
mainWindow.show();
}
});
app.on("before-quit", () => {
// Quit instantly if we are installing an update
if (installUpdate) {
return
// Quit instantly if we are installing an update
if (installUpdate) {
return;
}
if (process.platform === "darwin") {
forceQuit = true;
} else {
if (backend) {
backend.quit().then(() => {
mainWindow.close();
});
}
}
});
if (process.platform === "darwin") {
forceQuit = true
} else {
if (backend) {
backend.quit().then(() => {
mainWindow.close()
})
}
}
})
app.on("quit", () => {
})
app.on("quit", () => {});

View File

@ -1,68 +1,67 @@
let template = [
{
label: "Edit",
submenu: [
{ role: "undo" },
{ role: "redo" },
{ type: "separator" },
{ role: "cut" },
{ role: "copy" },
{ role: "paste" },
{ role: "pasteandmatchstyle" },
{ role: "delete" },
{ role: "selectall" }
]
},
{
label: "View",
submenu: [
{ role: "resetzoom" },
{ role: "zoomin" },
{ role: "zoomout" },
{ type: "separator" },
{ role: "togglefullscreen" }
]
},
{
role: "window",
submenu: [
{ role: "minimize" },
{ role: "close" }
]
},
{
role: "help",
submenu: [
{
label: "Learn More",
click () { require("electron").shell.openExternal("https://loki.network/") }
}
]
}
]
{
label: "Edit",
submenu: [
{ role: "undo" },
{ role: "redo" },
{ type: "separator" },
{ role: "cut" },
{ role: "copy" },
{ role: "paste" },
{ role: "pasteandmatchstyle" },
{ role: "delete" },
{ role: "selectall" }
]
},
{
label: "View",
submenu: [
{ role: "resetzoom" },
{ role: "zoomin" },
{ role: "zoomout" },
{ type: "separator" },
{ role: "togglefullscreen" }
]
},
{
role: "window",
submenu: [{ role: "minimize" }, { role: "close" }]
},
{
role: "help",
submenu: [
{
label: "Learn More",
click() {
require("electron").shell.openExternal("https://loki.network/");
}
}
]
}
];
if (process.platform === "darwin") {
template.unshift({
label: "Loki Electron Wallet",
submenu: [
{ role: "about" },
{ type: "separator" },
{ role: "hide" },
{ role: "hideothers" },
{ role: "unhide" },
{ type: "separator" },
{ role: "quit" }
]
})
// Window menu
template[3].submenu = [
{ role: "close" },
{ role: "minimize" },
{ role: "zoom" },
{ type: "separator" },
{ role: "front" }
template.unshift({
label: "Loki Electron Wallet",
submenu: [
{ role: "about" },
{ type: "separator" },
{ role: "hide" },
{ role: "hideothers" },
{ role: "unhide" },
{ type: "separator" },
{ role: "quit" }
]
});
// Window menu
template[3].submenu = [
{ role: "close" },
{ role: "minimize" },
{ role: "zoom" },
{ type: "separator" },
{ role: "front" }
];
}
export default template
export default template;

View File

@ -22,70 +22,70 @@
SOFTWARE.
*/
const crypto = require("crypto")
const crypto = require("crypto");
const ALGORITHM_NAME = "aes-128-gcm"
const ALGORITHM_NONCE_SIZE = 12
const ALGORITHM_TAG_SIZE = 16
const ALGORITHM_KEY_SIZE = 16
const PBKDF2_NAME = "sha256"
const PBKDF2_SALT_SIZE = 16
const PBKDF2_ITERATIONS = 32767
const ALGORITHM_NAME = "aes-128-gcm";
const ALGORITHM_NONCE_SIZE = 12;
const ALGORITHM_TAG_SIZE = 16;
const ALGORITHM_KEY_SIZE = 16;
const PBKDF2_NAME = "sha256";
const PBKDF2_SALT_SIZE = 16;
const PBKDF2_ITERATIONS = 32767;
export class SCEE {
encryptString (plaintext, password) {
// Generate a 128-bit salt using a CSPRNG.
let salt = crypto.randomBytes(PBKDF2_SALT_SIZE)
encryptString(plaintext, password) {
// Generate a 128-bit salt using a CSPRNG.
let salt = crypto.randomBytes(PBKDF2_SALT_SIZE);
// Derive a key using PBKDF2.
let key = crypto.pbkdf2Sync(new Buffer(password, "utf8"), salt, PBKDF2_ITERATIONS, ALGORITHM_KEY_SIZE, PBKDF2_NAME)
// Derive a key using PBKDF2.
let key = crypto.pbkdf2Sync(new Buffer(password, "utf8"), salt, PBKDF2_ITERATIONS, ALGORITHM_KEY_SIZE, PBKDF2_NAME);
// Encrypt and prepend salt.
let ciphertextAndNonceAndSalt = Buffer.concat([ salt, this.encrypt(new Buffer(plaintext, "utf8"), key) ])
// Encrypt and prepend salt.
let ciphertextAndNonceAndSalt = Buffer.concat([salt, this.encrypt(new Buffer(plaintext, "utf8"), key)]);
// Return as base64 string.
return ciphertextAndNonceAndSalt.toString("base64")
}
// Return as base64 string.
return ciphertextAndNonceAndSalt.toString("base64");
}
decryptString (base64CiphertextAndNonceAndSalt, password) {
// Decode the base64.
let ciphertextAndNonceAndSalt = new Buffer(base64CiphertextAndNonceAndSalt, "base64")
decryptString(base64CiphertextAndNonceAndSalt, password) {
// Decode the base64.
let ciphertextAndNonceAndSalt = new Buffer(base64CiphertextAndNonceAndSalt, "base64");
// Create buffers of salt and ciphertextAndNonce.
let salt = ciphertextAndNonceAndSalt.slice(0, PBKDF2_SALT_SIZE)
let ciphertextAndNonce = ciphertextAndNonceAndSalt.slice(PBKDF2_SALT_SIZE)
// Create buffers of salt and ciphertextAndNonce.
let salt = ciphertextAndNonceAndSalt.slice(0, PBKDF2_SALT_SIZE);
let ciphertextAndNonce = ciphertextAndNonceAndSalt.slice(PBKDF2_SALT_SIZE);
// Derive the key using PBKDF2.
let key = crypto.pbkdf2Sync(new Buffer(password, "utf8"), salt, PBKDF2_ITERATIONS, ALGORITHM_KEY_SIZE, PBKDF2_NAME)
// Derive the key using PBKDF2.
let key = crypto.pbkdf2Sync(new Buffer(password, "utf8"), salt, PBKDF2_ITERATIONS, ALGORITHM_KEY_SIZE, PBKDF2_NAME);
// Decrypt and return result.
return this.decrypt(ciphertextAndNonce, key).toString("utf8")
}
// Decrypt and return result.
return this.decrypt(ciphertextAndNonce, key).toString("utf8");
}
encrypt (plaintext, key) {
// Generate a 96-bit nonce using a CSPRNG.
let nonce = crypto.randomBytes(ALGORITHM_NONCE_SIZE)
encrypt(plaintext, key) {
// Generate a 96-bit nonce using a CSPRNG.
let nonce = crypto.randomBytes(ALGORITHM_NONCE_SIZE);
// Create the cipher instance.
let cipher = crypto.createCipheriv(ALGORITHM_NAME, key, nonce)
// Create the cipher instance.
let cipher = crypto.createCipheriv(ALGORITHM_NAME, key, nonce);
// Encrypt and prepend nonce.
let ciphertext = Buffer.concat([ cipher.update(plaintext), cipher.final() ])
// Encrypt and prepend nonce.
let ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
return Buffer.concat([ nonce, ciphertext, cipher.getAuthTag() ])
}
return Buffer.concat([nonce, ciphertext, cipher.getAuthTag()]);
}
decrypt (ciphertextAndNonce, key) {
// Create buffers of nonce, ciphertext and tag.
let nonce = ciphertextAndNonce.slice(0, ALGORITHM_NONCE_SIZE)
let ciphertext = ciphertextAndNonce.slice(ALGORITHM_NONCE_SIZE, ciphertextAndNonce.length - ALGORITHM_TAG_SIZE)
let tag = ciphertextAndNonce.slice(ciphertext.length + ALGORITHM_NONCE_SIZE)
decrypt(ciphertextAndNonce, key) {
// Create buffers of nonce, ciphertext and tag.
let nonce = ciphertextAndNonce.slice(0, ALGORITHM_NONCE_SIZE);
let ciphertext = ciphertextAndNonce.slice(ALGORITHM_NONCE_SIZE, ciphertextAndNonce.length - ALGORITHM_TAG_SIZE);
let tag = ciphertextAndNonce.slice(ciphertext.length + ALGORITHM_NONCE_SIZE);
// Create the cipher instance.
let cipher = crypto.createDecipheriv(ALGORITHM_NAME, key, nonce)
// Create the cipher instance.
let cipher = crypto.createDecipheriv(ALGORITHM_NAME, key, nonce);
// Decrypt and return result.
cipher.setAuthTag(tag)
return Buffer.concat([ cipher.update(ciphertext), cipher.final() ])
}
// Decrypt and return result.
cipher.setAuthTag(tag);
return Buffer.concat([cipher.update(ciphertext), cipher.final()]);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,487 +1,523 @@
import child_process from "child_process"
const request = require("request-promise")
const queue = require("promise-queue")
const http = require("http")
const fs = require("fs")
const path = require("upath")
const portscanner = require("portscanner")
import child_process from "child_process";
const request = require("request-promise");
const queue = require("promise-queue");
const http = require("http");
const fs = require("fs");
const path = require("upath");
const portscanner = require("portscanner");
export class Daemon {
constructor (backend) {
this.backend = backend
this.heartbeat = null
this.heartbeat_slow = null
this.id = 0
this.net_type = "mainnet"
this.local = false // do we have a local daemon ?
constructor(backend) {
this.backend = backend;
this.heartbeat = null;
this.heartbeat_slow = null;
this.id = 0;
this.net_type = "mainnet";
this.local = false; // do we have a local daemon ?
this.agent = new http.Agent({ keepAlive: true, maxSockets: 1 })
this.queue = new queue(1, Infinity)
this.agent = new http.Agent({ keepAlive: true, maxSockets: 1 });
this.queue = new queue(1, Infinity);
// Settings for timestamp to height conversion
// These are initial values used to calculate the height
this.PIVOT_BLOCK_HEIGHT = 119681
this.PIVOT_BLOCK_TIMESTAMP = 1539676273
this.PIVOT_BLOCK_TIME = 120
// Settings for timestamp to height conversion
// These are initial values used to calculate the height
this.PIVOT_BLOCK_HEIGHT = 119681;
this.PIVOT_BLOCK_TIMESTAMP = 1539676273;
this.PIVOT_BLOCK_TIME = 120;
}
checkVersion() {
return new Promise(resolve => {
if (process.platform === "win32") {
let lokid_path = path.join(__ryo_bin, "lokid.exe");
let lokid_version_cmd = `"${lokid_path}" --version`;
if (!fs.existsSync(lokid_path)) {
resolve(false);
}
child_process.exec(lokid_version_cmd, (error, stdout) => {
if (error) {
resolve(false);
}
resolve(stdout);
});
} else {
let lokid_path = path.join(__ryo_bin, "lokid");
let lokid_version_cmd = `"${lokid_path}" --version`;
if (!fs.existsSync(lokid_path)) {
resolve(false);
}
child_process.exec(lokid_version_cmd, { detached: true }, (error, stdout) => {
if (error) {
resolve(false);
}
resolve(stdout);
});
}
});
}
checkRemote(daemon) {
if (daemon.type === "local") {
return Promise.resolve({});
}
checkVersion () {
return new Promise((resolve, reject) => {
return this.sendRPC(
"get_info",
{},
{
protocol: "http://",
hostname: daemon.remote_host,
port: daemon.remote_port,
timeout: 20000
}
).then(data => {
if (data.error) return { error: data.error };
return {
net_type: data.result.nettype
};
});
}
start(options) {
const { net_type } = options.app;
const daemon = options.daemons[net_type];
if (daemon.type === "remote") {
this.local = false;
// save this info for later RPC calls
this.protocol = "http://";
this.hostname = daemon.remote_host;
this.port = daemon.remote_port;
return new Promise((resolve, reject) => {
// Set a 20 second timeout on get_info incase the node is unresponsive
this.sendRPC("get_info", {}, { timeout: 20000 }).then(data => {
if (!data.hasOwnProperty("error")) {
this.startHeartbeat();
resolve();
} else {
reject();
}
});
});
}
return new Promise((resolve, reject) => {
this.local = true;
const args = [
"--data-dir",
options.app.data_dir,
"--p2p-bind-ip",
daemon.p2p_bind_ip,
"--p2p-bind-port",
daemon.p2p_bind_port,
"--rpc-bind-ip",
daemon.rpc_bind_ip,
"--rpc-bind-port",
daemon.rpc_bind_port,
"--zmq-rpc-bind-ip",
daemon.zmq_rpc_bind_ip,
"--zmq-rpc-bind-port",
daemon.zmq_rpc_bind_port,
"--out-peers",
daemon.out_peers,
"--in-peers",
daemon.in_peers,
"--limit-rate-up",
daemon.limit_rate_up,
"--limit-rate-down",
daemon.limit_rate_down,
"--log-level",
daemon.log_level
];
const dirs = {
mainnet: options.app.data_dir,
stagenet: path.join(options.app.data_dir, "stagenet"),
testnet: path.join(options.app.data_dir, "testnet")
};
const { net_type } = options.app;
this.net_type = net_type;
if (net_type === "testnet") {
args.push("--testnet");
} else if (net_type === "stagenet") {
args.push("--stagenet");
}
args.push("--log-file", path.join(dirs[net_type], "logs", "lokid.log"));
if (daemon.rpc_bind_ip !== "127.0.0.1") {
args.push("--confirm-external-bind");
}
// TODO: Check if we need to push this command for staging too
if (daemon.type === "local_remote" && net_type === "mainnet") {
args.push("--bootstrap-daemon-address", `${daemon.remote_host}:${daemon.remote_port}`);
}
// save this info for later RPC calls
this.protocol = "http://";
this.hostname = daemon.rpc_bind_ip;
this.port = daemon.rpc_bind_port;
portscanner
.checkPortStatus(this.port, this.hostname)
.catch(() => "closed")
.then(status => {
if (status === "closed") {
if (process.platform === "win32") {
let lokid_path = path.join(__ryo_bin, "lokid.exe")
let lokid_version_cmd = `"${lokid_path}" --version`
if (!fs.existsSync(lokid_path)) { resolve(false) }
child_process.exec(lokid_version_cmd, (error, stdout, stderr) => {
if (error) { resolve(false) }
resolve(stdout)
})
this.daemonProcess = child_process.spawn(path.join(__ryo_bin, "lokid.exe"), args);
} else {
let lokid_path = path.join(__ryo_bin, "lokid")
let lokid_version_cmd = `"${lokid_path}" --version`
if (!fs.existsSync(lokid_path)) { resolve(false) }
child_process.exec(lokid_version_cmd, { detached: true }, (error, stdout, stderr) => {
if (error) { resolve(false) }
resolve(stdout)
})
}
})
}
checkRemote (daemon) {
if (daemon.type === "local") {
return Promise.resolve({})
}
return this.sendRPC("get_info", {}, {
protocol: "http://",
hostname: daemon.remote_host,
port: daemon.remote_port,
timeout: 20000
}).then(data => {
if (data.error) return { error: data.error }
return {
net_type: data.result.nettype
}
})
}
start (options) {
const { net_type } = options.app
const daemon = options.daemons[net_type]
if (daemon.type === "remote") {
this.local = false
// save this info for later RPC calls
this.protocol = "http://"
this.hostname = daemon.remote_host
this.port = daemon.remote_port
return new Promise((resolve, reject) => {
// Set a 20 second timeout on get_info incase the node is unresponsive
this.sendRPC("get_info", {}, { timeout: 20000 }).then((data) => {
if (!data.hasOwnProperty("error")) {
this.startHeartbeat()
resolve()
} else {
reject()
}
})
})
}
return new Promise((resolve, reject) => {
this.local = true
const args = [
"--data-dir", options.app.data_dir,
"--p2p-bind-ip", daemon.p2p_bind_ip,
"--p2p-bind-port", daemon.p2p_bind_port,
"--rpc-bind-ip", daemon.rpc_bind_ip,
"--rpc-bind-port", daemon.rpc_bind_port,
"--zmq-rpc-bind-ip", daemon.zmq_rpc_bind_ip,
"--zmq-rpc-bind-port", daemon.zmq_rpc_bind_port,
"--out-peers", daemon.out_peers,
"--in-peers", daemon.in_peers,
"--limit-rate-up", daemon.limit_rate_up,
"--limit-rate-down", daemon.limit_rate_down,
"--log-level", daemon.log_level
]
const dirs = {
"mainnet": options.app.data_dir,
"stagenet": path.join(options.app.data_dir, "stagenet"),
"testnet": path.join(options.app.data_dir, "testnet")
this.daemonProcess = child_process.spawn(path.join(__ryo_bin, "lokid"), args, {
detached: true
});
}
const { net_type } = options.app
this.net_type = net_type
this.daemonProcess.stdout.on("data", data => process.stdout.write(`Daemon: ${data}`));
this.daemonProcess.on("error", err => process.stderr.write(`Daemon: ${err}`));
this.daemonProcess.on("close", code => {
process.stderr.write(`Daemon: exited with code ${code} \n`);
this.daemonProcess = null;
this.agent.destroy();
if (code === null) {
reject(new Error("Failed to start local daemon"));
}
});
if (net_type === "testnet") {
args.push("--testnet")
} else if (net_type === "stagenet") {
args.push("--stagenet")
}
args.push("--log-file", path.join(dirs[net_type], "logs", "lokid.log"))
if (daemon.rpc_bind_ip !== "127.0.0.1") { args.push("--confirm-external-bind") }
// TODO: Check if we need to push this command for staging too
if (daemon.type === "local_remote" && net_type === "mainnet") {
args.push(
"--bootstrap-daemon-address",
`${daemon.remote_host}:${daemon.remote_port}`
)
}
// save this info for later RPC calls
this.protocol = "http://"
this.hostname = daemon.rpc_bind_ip
this.port = daemon.rpc_bind_port
portscanner.checkPortStatus(this.port, this.hostname).catch(e => "closed").then(status => {
if (status === "closed") {
if (process.platform === "win32") {
this.daemonProcess = child_process.spawn(path.join(__ryo_bin, "lokid.exe"), args)
} else {
this.daemonProcess = child_process.spawn(path.join(__ryo_bin, "lokid"), args, {
detached: true
})
}
this.daemonProcess.stdout.on("data", data => process.stdout.write(`Daemon: ${data}`))
this.daemonProcess.on("error", err => process.stderr.write(`Daemon: ${err}`))
this.daemonProcess.on("close", code => {
process.stderr.write(`Daemon: exited with code ${code} \n`)
this.daemonProcess = null
this.agent.destroy()
if (code === null) {
reject(new Error("Failed to start local daemon"))
}
})
// To let caller know when the daemon is ready
// We can't apply timeout to this because the local daemon might be syncing in the background
let intrvl = setInterval(() => {
this.sendRPC("get_info").then((data) => {
if (!data.hasOwnProperty("error")) {
this.startHeartbeat()
clearInterval(intrvl)
resolve()
} else {
if (this.daemonProcess && data.error.cause &&
data.error.cause.code === "ECONNREFUSED") {
// Ignore
} else {
clearInterval(intrvl)
this.killProcess()
reject(new Error("Could not connect to local daemon"))
}
}
})
}, 1000)
// To let caller know when the daemon is ready
// We can't apply timeout to this because the local daemon might be syncing in the background
let intrvl = setInterval(() => {
this.sendRPC("get_info").then(data => {
if (!data.hasOwnProperty("error")) {
this.startHeartbeat();
clearInterval(intrvl);
resolve();
} else {
reject(new Error(`Local daemon port ${this.port} is in use`))
if (this.daemonProcess && data.error.cause && data.error.cause.code === "ECONNREFUSED") {
// Ignore
} else {
clearInterval(intrvl);
this.killProcess();
reject(new Error("Could not connect to local daemon"));
}
}
})
})
});
}, 1000);
} else {
reject(new Error(`Local daemon port ${this.port} is in use`));
}
});
});
}
killProcess() {
if (this.daemonProcess) {
this.daemonProcess.kill();
this.daemonProcess = null;
}
}
handle(data) {
let params = data.data;
switch (data.method) {
case "ban_peer":
this.banPeer(params.host, params.seconds);
break;
default:
}
}
banPeer(host, seconds = 3600) {
if (!seconds) {
seconds = 3600;
}
killProcess () {
if (this.daemonProcess) {
this.daemonProcess.kill()
this.daemonProcess = null
let params = {
bans: [
{
host,
seconds,
ban: true
}
}
]
};
handle (data) {
let params = data.data
this.sendRPC("set_bans", params).then(data => {
if (data.hasOwnProperty("error") || !data.hasOwnProperty("result")) {
this.sendGateway("show_notification", {
type: "negative",
i18n: "notification.errors.banningPeer",
timeout: 2000
});
return;
}
switch (data.method) {
case "ban_peer":
this.banPeer(params.host, params.seconds)
break
let end_time = new Date(Date.now() + seconds * 1000).toLocaleString();
this.sendGateway("show_notification", {
i18n: ["notification.positive.bannedPeer", { host, time: end_time }],
timeout: 2000
});
default:
}
}
// Send updated peer and ban list
this.heartbeatSlowAction();
});
}
banPeer (host, seconds = 3600) {
if (!seconds) { seconds = 3600 }
timestampToHeight(timestamp, pivot = null, recursion_limit = null) {
return new Promise((resolve, reject) => {
if (timestamp > 999999999999) {
// We have got a JS ms timestamp, convert
timestamp = Math.floor(timestamp / 1000);
}
let params = {
bans: [{
host,
seconds,
ban: true
}]
pivot = pivot || [this.PIVOT_BLOCK_HEIGHT, this.PIVOT_BLOCK_TIMESTAMP];
recursion_limit = recursion_limit || 0;
let diff = Math.floor((timestamp - pivot[1]) / this.PIVOT_BLOCK_TIME);
let estimated_height = pivot[0] + diff;
if (estimated_height <= 0) {
return resolve(0);
}
if (recursion_limit > 10) {
return resolve(pivot[0]);
}
this.getRPC("block_header_by_height", {
height: estimated_height
}).then(data => {
if (data.hasOwnProperty("error") || !data.hasOwnProperty("result")) {
if (data.error.code == -2) {
// Too big height
this.getRPC("last_block_header").then(data => {
if (data.hasOwnProperty("error") || !data.hasOwnProperty("result")) {
return reject();
}
let new_pivot = [data.result.block_header.height, data.result.block_header.timestamp];
// If we are within an hour that is good enough
// If for some reason there is a > 1h gap between blocks
// the recursion limit will take care of infinite loop
if (Math.abs(timestamp - new_pivot[1]) < 3600) {
return resolve(new_pivot[0]);
}
// Continue recursion with new pivot
resolve(new_pivot);
});
return;
} else {
return reject();
}
}
this.sendRPC("set_bans", params).then((data) => {
if (data.hasOwnProperty("error") || !data.hasOwnProperty("result")) {
this.sendGateway("show_notification", {
type: "negative",
i18n: "notification.errors.banningPeer",
timeout: 2000
})
return
}
let new_pivot = [data.result.block_header.height, data.result.block_header.timestamp];
let end_time = new Date(Date.now() + seconds * 1000).toLocaleString()
this.sendGateway("show_notification", {
i18n: ["notification.positive.bannedPeer", { host, time: end_time }],
timeout: 2000
})
// Send updated peer and ban list
this.heartbeatSlowAction()
})
}
timestampToHeight (timestamp, pivot = null, recursion_limit = null) {
return new Promise((resolve, reject) => {
if (timestamp > 999999999999) {
// We have got a JS ms timestamp, convert
timestamp = Math.floor(timestamp / 1000)
}
pivot = pivot || [this.PIVOT_BLOCK_HEIGHT, this.PIVOT_BLOCK_TIMESTAMP]
recursion_limit = recursion_limit || 0
let diff = Math.floor((timestamp - pivot[1]) / this.PIVOT_BLOCK_TIME)
let estimated_height = pivot[0] + diff
if (estimated_height <= 0) {
return resolve(0)
}
if (recursion_limit > 10) {
return resolve(pivot[0])
}
this.getRPC("block_header_by_height", { height: estimated_height }).then((data) => {
if (data.hasOwnProperty("error") || !data.hasOwnProperty("result")) {
if (data.error.code == -2) { // Too big height
this.getRPC("last_block_header").then((data) => {
if (data.hasOwnProperty("error") || !data.hasOwnProperty("result")) {
return reject()
}
let new_pivot = [data.result.block_header.height, data.result.block_header.timestamp]
// If we are within an hour that is good enough
// If for some reason there is a > 1h gap between blocks
// the recursion limit will take care of infinite loop
if (Math.abs(timestamp - new_pivot[1]) < 3600) {
return resolve(new_pivot[0])
}
// Continue recursion with new pivot
resolve(new_pivot)
})
return
} else {
return reject()
}
}
let new_pivot = [data.result.block_header.height, data.result.block_header.timestamp]
// If we are within an hour that is good enough
// If for some reason there is a > 1h gap between blocks
// the recursion limit will take care of infinite loop
if (Math.abs(timestamp - new_pivot[1]) < 3600) {
return resolve(new_pivot[0])
}
// Continue recursion with new pivot
resolve(new_pivot)
})
}).then((pivot_or_height) => {
return Array.isArray(pivot_or_height)
? this.timestampToHeight(timestamp, pivot_or_height, recursion_limit + 1)
: pivot_or_height
}).catch(e => {
return false
})
}
startHeartbeat () {
clearInterval(this.heartbeat)
this.heartbeat = setInterval(() => {
this.heartbeatAction()
}, this.local ? 5 * 1000 : 30 * 1000) // 5 seconds for local daemon, 30 seconds for remote
this.heartbeatAction()
clearInterval(this.heartbeat_slow)
this.heartbeat_slow = setInterval(() => {
this.heartbeatSlowAction()
}, 30 * 1000) // 30 seconds
this.heartbeatSlowAction()
clearInterval(this.serviceNodeHeartbeat)
this.serviceNodeHeartbeat = setInterval(() => {
this.updateServiceNodes()
}, 5 * 60 * 1000) // 5 minutes
this.updateServiceNodes()
}
heartbeatAction () {
let actions = []
// No difference between local and remote heartbeat action for now
if (this.local) {
actions = [
this.getRPC("info")
]
} else {
actions = [
this.getRPC("info")
]
// If we are within an hour that is good enough
// If for some reason there is a > 1h gap between blocks
// the recursion limit will take care of infinite loop
if (Math.abs(timestamp - new_pivot[1]) < 3600) {
return resolve(new_pivot[0]);
}
Promise.all(actions).then((data) => {
let daemon_info = {
}
for (let n of data) {
if (n == undefined || !n.hasOwnProperty("result") || n.result == undefined) { continue }
if (n.method == "get_info") {
daemon_info.info = n.result
}
}
this.sendGateway("set_daemon_data", daemon_info)
})
// Continue recursion with new pivot
resolve(new_pivot);
});
})
.then(pivot_or_height => {
return Array.isArray(pivot_or_height)
? this.timestampToHeight(timestamp, pivot_or_height, recursion_limit + 1)
: pivot_or_height;
})
.catch(() => {
return false;
});
}
startHeartbeat() {
clearInterval(this.heartbeat);
this.heartbeat = setInterval(
() => {
this.heartbeatAction();
},
this.local ? 5 * 1000 : 30 * 1000
); // 5 seconds for local daemon, 30 seconds for remote
this.heartbeatAction();
clearInterval(this.heartbeat_slow);
this.heartbeat_slow = setInterval(() => {
this.heartbeatSlowAction();
}, 30 * 1000); // 30 seconds
this.heartbeatSlowAction();
clearInterval(this.serviceNodeHeartbeat);
this.serviceNodeHeartbeat = setInterval(() => {
this.updateServiceNodes();
}, 5 * 60 * 1000); // 5 minutes
this.updateServiceNodes();
}
heartbeatAction() {
let actions = [];
// No difference between local and remote heartbeat action for now
if (this.local) {
actions = [this.getRPC("info")];
} else {
actions = [this.getRPC("info")];
}
heartbeatSlowAction () {
let actions = []
if (this.local) {
actions = [
this.getRPC("connections"),
this.getRPC("bans")
// this.getRPC("txpool_backlog"),
]
} else {
actions = [
// this.getRPC("txpool_backlog"),
]
Promise.all(actions).then(data => {
let daemon_info = {};
for (let n of data) {
if (n == undefined || !n.hasOwnProperty("result") || n.result == undefined) {
continue;
}
if (actions.length === 0) return
Promise.all(actions).then((data) => {
let daemon_info = {
}
for (let n of data) {
if (n == undefined || !n.hasOwnProperty("result") || n.result == undefined) { continue }
if (n.method == "get_connections" && n.result.hasOwnProperty("connections")) {
daemon_info.connections = n.result.connections
} else if (n.method == "get_bans" && n.result.hasOwnProperty("bans")) {
daemon_info.bans = n.result.bans
} else if (n.method == "get_txpool_backlog" && n.result.hasOwnProperty("backlog")) {
daemon_info.tx_pool_backlog = n.result.backlog
}
}
this.sendGateway("set_daemon_data", daemon_info)
})
}
updateServiceNodes () {
// Get the latest service node data
this.getRPC("service_nodes").then(data => {
if (!data.hasOwnProperty("result")) return
const service_nodes = data.result.service_node_states
this.sendGateway("set_daemon_data", { service_nodes })
})
}
sendGateway (method, data) {
this.backend.send(method, data)
}
sendRPC (method, params = {}, options = {}) {
let id = this.id++
const protocol = options.protocol || this.protocol
const hostname = options.hostname || this.hostname
const port = options.port || this.port
let requestOptions = {
uri: `${protocol}${hostname}:${port}/json_rpc`,
method: "POST",
json: {
jsonrpc: "2.0",
id: id,
method: method
},
agent: this.agent
}
if (Object.keys(params).length !== 0) {
requestOptions.json.params = params
if (n.method == "get_info") {
daemon_info.info = n.result;
}
}
this.sendGateway("set_daemon_data", daemon_info);
});
}
// If there's a timeout then set it
if (options.timeout) {
requestOptions.timeout = options.timeout
}
heartbeatSlowAction() {
let actions = [];
if (this.local) {
actions = [
this.getRPC("connections"),
this.getRPC("bans")
// this.getRPC("txpool_backlog"),
];
} else {
actions = [
// this.getRPC("txpool_backlog"),
];
}
return this.queue.add(() => {
return request(requestOptions)
.then((response) => {
if (response.hasOwnProperty("error")) {
return {
method: method,
params: params,
error: response.error
}
}
return {
method: method,
params: params,
result: response.result
}
}).catch(error => {
return {
method: method,
params: params,
error: {
code: -1,
message: "Cannot connect to daemon-rpc",
cause: error.cause
}
}
})
if (actions.length === 0) return;
Promise.all(actions).then(data => {
let daemon_info = {};
for (let n of data) {
if (n == undefined || !n.hasOwnProperty("result") || n.result == undefined) {
continue;
}
if (n.method == "get_connections" && n.result.hasOwnProperty("connections")) {
daemon_info.connections = n.result.connections;
} else if (n.method == "get_bans" && n.result.hasOwnProperty("bans")) {
daemon_info.bans = n.result.bans;
} else if (n.method == "get_txpool_backlog" && n.result.hasOwnProperty("backlog")) {
daemon_info.tx_pool_backlog = n.result.backlog;
}
}
this.sendGateway("set_daemon_data", daemon_info);
});
}
updateServiceNodes() {
// Get the latest service node data
this.getRPC("service_nodes").then(data => {
if (!data.hasOwnProperty("result")) return;
const service_nodes = data.result.service_node_states;
this.sendGateway("set_daemon_data", { service_nodes });
});
}
sendGateway(method, data) {
this.backend.send(method, data);
}
sendRPC(method, params = {}, options = {}) {
let id = this.id++;
const protocol = options.protocol || this.protocol;
const hostname = options.hostname || this.hostname;
const port = options.port || this.port;
let requestOptions = {
uri: `${protocol}${hostname}:${port}/json_rpc`,
method: "POST",
json: {
jsonrpc: "2.0",
id: id,
method: method
},
agent: this.agent
};
if (Object.keys(params).length !== 0) {
requestOptions.json.params = params;
}
// If there's a timeout then set it
if (options.timeout) {
requestOptions.timeout = options.timeout;
}
return this.queue.add(() => {
return request(requestOptions)
.then(response => {
if (response.hasOwnProperty("error")) {
return {
method: method,
params: params,
error: response.error
};
}
return {
method: method,
params: params,
result: response.result
};
})
}
/**
* Call one of the get_* RPC calls
*/
getRPC (parameter, args) {
return this.sendRPC(`get_${parameter}`, args)
}
quit () {
clearInterval(this.heartbeat)
return new Promise((resolve, reject) => {
if (this.daemonProcess) {
this.daemonProcess.on("close", code => {
this.agent.destroy()
clearTimeout(this.forceKill)
resolve()
})
// Force kill after 20 seconds
this.forceKill = setTimeout(() => {
if (this.daemonProcess) {
this.daemonProcess.kill("SIGKILL")
}
}, 20000)
const signal = this.isDaemonSyncing ? "SIGKILL" : "SIGTERM"
this.daemonProcess.kill(signal)
} else {
resolve()
.catch(error => {
return {
method: method,
params: params,
error: {
code: -1,
message: "Cannot connect to daemon-rpc",
cause: error.cause
}
})
}
};
});
});
}
/**
* Call one of the get_* RPC calls
*/
getRPC(parameter, args) {
return this.sendRPC(`get_${parameter}`, args);
}
quit() {
clearInterval(this.heartbeat);
return new Promise(resolve => {
if (this.daemonProcess) {
this.daemonProcess.on("close", () => {
this.agent.destroy();
clearTimeout(this.forceKill);
resolve();
});
// Force kill after 20 seconds
this.forceKill = setTimeout(() => {
if (this.daemonProcess) {
this.daemonProcess.kill("SIGKILL");
}
}, 20000);
const signal = this.isDaemonSyncing ? "SIGKILL" : "SIGTERM";
this.daemonProcess.kill(signal);
} else {
resolve();
}
});
}
}

View File

@ -1,3 +1,3 @@
export const WALLET_NOT_OPEN = -1
export const WALLET_OPEN = 0
export const WALLET_ERROR = 1
export const WALLET_NOT_OPEN = -1;
export const WALLET_OPEN = 0;
export const WALLET_ERROR = 1;

File diff suppressed because it is too large Load Diff

View File

@ -1,33 +1,27 @@
<template>
<div id="q-app">
<div id="q-app">
<router-view />
</div>
</div>
</template>
<script>
import { mapState } from "vuex"
import { mapState } from "vuex";
export default {
name: "App",
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
}),
watch: {
theme: function () {
if(this.theme == "dark")
document.body.classList.add("dark")
else
document.body.classList.remove("dark")
}
},
mounted () {
if(this.theme == "dark")
document.body.classList.add("dark")
else
document.body.classList.remove("dark")
name: "App",
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme
}),
watch: {
theme: function() {
if (this.theme == "dark") document.body.classList.add("dark");
else document.body.classList.remove("dark");
}
}
},
mounted() {
if (this.theme == "dark") document.body.classList.add("dark");
else document.body.classList.remove("dark");
}
};
</script>
<style>
</style>
<style></style>

View File

@ -1,252 +1,238 @@
<template>
<q-modal v-model="isVisible" maximized class="address-book-details">
<q-modal v-model="isVisible" maximized class="address-book-details">
<q-modal-layout v-if="mode == 'edit' || mode == 'new'">
<q-toolbar slot="header" color="dark" inverted>
<q-btn flat round dense icon="reply" @click="close()" />
<q-toolbar-title v-if="mode=='new'">
{{ $t("strings.addAddressBookEntry") }}
</q-toolbar-title>
<q-toolbar-title v-else-if="mode=='edit'">
{{ $t("strings.editAddressBookEntry") }}
</q-toolbar-title>
<q-toolbar slot="header" color="dark" inverted>
<q-btn flat round dense icon="reply" @click="close()" />
<q-toolbar-title v-if="mode == 'new'">
{{ $t("strings.addAddressBookEntry") }}
</q-toolbar-title>
<q-toolbar-title v-else-if="mode == 'edit'">
{{ $t("strings.editAddressBookEntry") }}
</q-toolbar-title>
<q-btn v-if="mode=='edit'" flat no-ripple @click="cancelEdit()" :label="$t('buttons.cancel')" />
<q-btn class="q-ml-sm" color="primary" @click="save()" :label="$t('buttons.save')" />
<q-btn v-if="mode == 'edit'" flat no-ripple :label="$t('buttons.cancel')" @click="cancelEdit()" />
<q-btn class="q-ml-sm" color="primary" :label="$t('buttons.save')" @click="save()" />
</q-toolbar>
<div class="address-book-modal q-mx-md">
<LokiField :label="$t('fieldLabels.address')" :error="$v.newEntry.address.$error">
<q-input
v-model.trim="newEntry.address"
:placeholder="address_placeholder"
:dark="theme == 'dark'"
hide-underline
@blur="$v.newEntry.address.$touch"
/>
<q-checkbox
v-model="newEntry.starred"
checked-icon="star"
unchecked-icon="star_border"
class="star-entry"
dark
/>
</LokiField>
<LokiField :label="$t('fieldLabels.name')">
<q-input v-model.trim="newEntry.name" :dark="theme == 'dark'" hide-underline />
</LokiField>
<LokiField :label="$t('fieldLabels.paymentId')" :error="$v.newEntry.payment_id.$error" optional>
<q-input
v-model.trim="newEntry.payment_id"
:placeholder="
$t('placeholders.hexCharacters', {
count: '16 or 64'
})
"
:dark="theme == 'dark'"
hide-underline
@blur="$v.newEntry.payment_id.$touch"
/>
</LokiField>
<LokiField :label="$t('fieldLabels.notes')" optional>
<q-input
v-model="newEntry.description"
:placeholder="$t('placeholders.additionalNotes')"
type="textarea"
:dark="theme == 'dark'"
hide-underline
/>
</LokiField>
</q-toolbar>
<div class="address-book-modal q-mx-md">
<LokiField :label="$t('fieldLabels.address')" :error="$v.newEntry.address.$error">
<q-input
v-model.trim="newEntry.address"
:placeholder="address_placeholder"
@blur="$v.newEntry.address.$touch"
:dark="theme=='dark'"
hide-underline
/>
<q-checkbox
v-model="newEntry.starred"
checked-icon="star"
unchecked-icon="star_border"
class="star-entry"
dark
/>
</LokiField>
<LokiField :label="$t('fieldLabels.name')">
<q-input
v-model.trim="newEntry.name"
:dark="theme=='dark'"
hide-underline
/>
</LokiField>
<LokiField :label="$t('fieldLabels.paymentId')" :error="$v.newEntry.payment_id.$error" optional>
<q-input
v-model.trim="newEntry.payment_id"
:placeholder="$t('placeholders.hexCharacters', { count: '16 or 64' })"
@blur="$v.newEntry.payment_id.$touch"
:dark="theme=='dark'"
hide-underline
/>
</LokiField>
<LokiField :label="$t('fieldLabels.notes')" optional>
<q-input
v-model="newEntry.description"
:placeholder="$t('placeholders.additionalNotes')"
type="textarea"
:dark="theme=='dark'"
hide-underline
/>
</LokiField>
<q-field v-if="mode=='edit'">
<q-btn class="float-right" color="red" @click="deleteEntry()" :label="$t('buttons.delete')" />
</q-field>
</div>
<q-field v-if="mode == 'edit'">
<q-btn class="float-right" color="red" :label="$t('buttons.delete')" @click="deleteEntry()" />
</q-field>
</div>
</q-modal-layout>
<q-modal-layout v-else>
<q-toolbar slot="header" color="dark" inverted>
<q-btn flat round dense icon="reply" @click="close()" />
<q-toolbar-title>
{{ $t('strings.addressBookDetails') }}
</q-toolbar-title>
<q-btn class="q-mr-sm"
flat no-ripple
:disable="!is_ready"
@click="edit()" :label="$t('buttons.edit')" />
<q-btn
color="primary"
:disabled="view_only"
@click="sendToAddress"
:label="$t('buttons.sendCoins')" />
</q-toolbar>
<div class="layout-padding">
<q-toolbar slot="header" color="dark" inverted>
<q-btn flat round dense icon="reply" @click="close()" />
<q-toolbar-title>
{{ $t("strings.addressBookDetails") }}
</q-toolbar-title>
<q-btn class="q-mr-sm" flat no-ripple :disable="!is_ready" :label="$t('buttons.edit')" @click="edit()" />
<q-btn color="primary" :disabled="view_only" :label="$t('buttons.sendCoins')" @click="sendToAddress" />
</q-toolbar>
<div class="layout-padding">
<template v-if="entry != null">
<AddressHeader
:address="entry.address"
:title="entry.name"
:payment_id="entry.payment_id"
:extra="entry.description ? $t('strings.notes') + ': ' + entry.description : ''"
/>
<template v-if="entry != null">
<div class="q-mt-lg">
<div class="non-selectable">
<q-icon name="history" size="24px" />
<span class="vertical-middle q-ml-xs">{{ $t("strings.recentTransactionsWithAddress") }}</span>
</div>
<AddressHeader :address="entry.address"
:title="entry.name"
:payment_id="entry.payment_id"
:extra="entry.description ? $t('strings.notes')+': '+entry.description : ''"
/>
<div class="q-mt-lg">
<div class="non-selectable">
<q-icon name="history" size="24px" />
<span class="vertical-middle q-ml-xs">{{ $t('strings.recentTransactionsWithAddress') }}</span>
</div>
<TxList type="all_in" :limit="5" :to-outgoing-address="entry.address" :key="entry.address"/>
</div>
</template>
</div>
<TxList :key="entry.address" type="all_in" :limit="5" :to-outgoing-address="entry.address" />
</div>
</template>
</div>
</q-modal-layout>
</q-modal>
</q-modal>
</template>
<script>
import { mapState } from "vuex"
import Identicon from "components/identicon"
import AddressHeader from "components/address_header"
import TxList from "components/tx_list"
import LokiField from "components/loki_field"
import { payment_id, address } from "src/validators/common"
import { required } from "vuelidate/lib/validators"
import { mapState } from "vuex";
import AddressHeader from "components/address_header";
import TxList from "components/tx_list";
import LokiField from "components/loki_field";
import { payment_id, address } from "src/validators/common";
import { required } from "vuelidate/lib/validators";
export default {
name: "AddressBookDetails",
data () {
return {
isVisible: false,
entry: null,
mode: "view",
newEntry: {
index: false,
address: "",
payment_id: "",
name: "",
description: "",
starred: false
}
}
name: "AddressBookDetails",
components: {
AddressHeader,
TxList,
LokiField
},
data() {
return {
isVisible: false,
entry: null,
mode: "view",
newEntry: {
index: false,
address: "",
payment_id: "",
name: "",
description: "",
starred: false
}
};
},
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
view_only: state => state.gateway.wallet.info.view_only,
is_ready() {
return this.$store.getters["gateway/isReady"];
},
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
view_only: state => state.gateway.wallet.info.view_only,
is_ready (state) {
return this.$store.getters["gateway/isReady"]
},
address_placeholder (state) {
const wallet = state.gateway.wallet.info;
const prefix = (wallet && wallet.address && wallet.address[0]) || "L";
return `${prefix}..`;
}
}),
validations: {
newEntry: {
address: {
required,
isAddress(value) {
if (value === '') return true
return new Promise(resolve => {
address(value, this.$gateway)
.then(() => resolve(true))
.catch(e => resolve(false))
});
}
},
payment_id: { payment_id }
}
},
methods: {
save () {
this.$v.newEntry.$touch()
if (this.$v.newEntry.address.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.invalidAddress")
})
return
}
if (this.$v.newEntry.payment_id.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.invalidPaymentId")
})
return
}
this.$gateway.send("wallet", "add_address_book", this.newEntry)
this.close()
},
deleteEntry () {
this.$gateway.send("wallet", "delete_address_book", this.newEntry)
this.close()
},
sendToAddress () {
this.close()
this.$router.replace({ path: "send", query: {address: this.entry.address, payment_id: this.entry.payment_id} });
},
edit () {
this.mode = "edit"
this.newEntry = this.entry
},
cancelEdit () {
this.mode = "view"
this.$v.$reset();
this.newEntry = {
index: false,
address: "",
payment_id: "",
name: "",
description: "",
starred: false
}
},
close () {
this.isVisible = false
this.$v.$reset();
this.newEntry = {
index: false,
address: "",
payment_id: "",
name: "",
description: "",
starred: false
}
}
},
components: {
AddressHeader,
Identicon,
TxList,
LokiField
address_placeholder(state) {
const wallet = state.gateway.wallet.info;
const prefix = (wallet && wallet.address && wallet.address[0]) || "L";
return `${prefix}..`;
}
}
}),
validations: {
newEntry: {
address: {
required,
isAddress(value) {
if (value === "") return true;
return new Promise(resolve => {
address(value, this.$gateway)
.then(() => resolve(true))
.catch(() => resolve(false));
});
}
},
payment_id: { payment_id }
}
},
methods: {
save() {
this.$v.newEntry.$touch();
if (this.$v.newEntry.address.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.invalidAddress")
});
return;
}
if (this.$v.newEntry.payment_id.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.invalidPaymentId")
});
return;
}
this.$gateway.send("wallet", "add_address_book", this.newEntry);
this.close();
},
deleteEntry() {
this.$gateway.send("wallet", "delete_address_book", this.newEntry);
this.close();
},
sendToAddress() {
this.close();
this.$router.replace({
path: "send",
query: {
address: this.entry.address,
payment_id: this.entry.payment_id
}
});
},
edit() {
this.mode = "edit";
this.newEntry = this.entry;
},
cancelEdit() {
this.mode = "view";
this.$v.$reset();
this.newEntry = {
index: false,
address: "",
payment_id: "",
name: "",
description: "",
starred: false
};
},
close() {
this.isVisible = false;
this.$v.$reset();
this.newEntry = {
index: false,
address: "",
payment_id: "",
name: "",
description: "",
starred: false
};
}
}
};
</script>
<style lang="scss">
.address-book-details {
.address-book-modal {
> .loki-field {
margin-top: 16px;
}
.star-entry {
padding: 4px;
}
.address-book-modal {
> .loki-field {
margin-top: 16px;
}
.star-entry {
padding: 4px;
}
}
}
</style>

View File

@ -1,198 +1,202 @@
<template>
<q-modal v-model="isVisible" maximized>
<q-modal v-model="isVisible" maximized>
<q-modal-layout>
<q-toolbar slot="header" color="dark" inverted>
<q-btn
flat
round
dense
@click="isVisible = false"
icon="reply"
/>
<q-toolbar-title>
{{ $t("titles.addressDetails") }}
</q-toolbar-title>
<q-btn flat @click="isQRCodeVisible = true" :label="$t('buttons.showQRCode')" />
<q-btn class="q-ml-sm" color="primary" @click="copyAddress()" :label="$t('buttons.copyAddress')" />
</q-toolbar>
<q-toolbar slot="header" color="dark" inverted>
<q-btn flat round dense icon="reply" @click="isVisible = false" />
<q-toolbar-title>
{{ $t("titles.addressDetails") }}
</q-toolbar-title>
<q-btn flat :label="$t('buttons.showQRCode')" @click="isQRCodeVisible = true" />
<q-btn class="q-ml-sm" color="primary" :label="$t('buttons.copyAddress')" @click="copyAddress()" />
</q-toolbar>
<div class="layout-padding">
<template v-if="address != null">
<AddressHeader :address="address.address"
:title="addressHeaderInfo.title"
:extra="addressHeaderInfo.extra"
:showCopy="false"
/>
<template v-if="address.used">
<div class="row justify-between" style="max-width: 768px">
<div class="infoBox">
<div class="infoBoxContent">
<div class="text"><span>{{ $t("strings.lokiBalance") }}</span></div>
<div class="value"><span><FormatLoki :amount="address.balance" /></span></div>
</div>
</div>
<div class="infoBox">
<div class="infoBoxContent">
<div class="text"><span>{{ $t("strings.lokiUnlockedBalance") }}</span></div>
<div class="value"><span><FormatLoki :amount="address.unlocked_balance" /></span></div>
</div>
</div>
<div class="infoBox">
<div class="infoBoxContent">
<div class="text"><span>{{ $t("strings.numberOfUnspentOutputs") }}</span></div>
<div class="value"><span>{{ address.num_unspent_outputs }}</span></div>
</div>
</div>
</div>
</template>
<template v-else>
<div class="row justify-between" style="max-width: 768px">
<div class="infoBox">
<div class="infoBoxContent">
<div class="text"><span>{{ $t("strings.lokiBalance") }}</span></div>
<div class="value"><span>N/A</span></div>
</div>
</div>
<div class="infoBox">
<div class="infoBoxContent">
<div class="text"><span>{{ $t("strings.lokiUnlockedBalance") }}</span></div>
<div class="value"><span>N/A</span></div>
</div>
</div>
<div class="infoBox">
<div class="infoBoxContent">
<div class="text"><span>{{ $t("strings.numberOfUnspentOutputs") }}</span></div>
<div class="value"><span>N/A</span></div>
</div>
</div>
</div>
</template>
<div class="q-mt-sm">
<div class="non-selectable">
<q-icon name="history" size="24px" />
<span class="vertical-middle q-ml-xs">{{ $t("strings.recentIncomingTransactionsToAddress") }}</span>
</div>
<div style="margin: 0 -16px;">
<TxList type="all_in" :limit="5" :to-incoming-address-index="address.address_index" :key="address.address"/>
</div>
<div class="layout-padding">
<template v-if="address != null">
<AddressHeader
:address="address.address"
:title="addressHeaderInfo.title"
:extra="addressHeaderInfo.extra"
:show-copy="false"
/>
<template v-if="address.used">
<div class="row justify-between" style="max-width: 768px">
<div class="infoBox">
<div class="infoBoxContent">
<div class="text">
<span>{{ $t("strings.lokiBalance") }}</span>
</div>
<div class="value">
<span><FormatLoki :amount="address.balance"/></span>
</div>
</div>
</div>
</template>
<div class="infoBox">
<div class="infoBoxContent">
<div class="text">
<span>{{ $t("strings.lokiUnlockedBalance") }}</span>
</div>
<div class="value">
<span><FormatLoki :amount="address.unlocked_balance"/></span>
</div>
</div>
</div>
</div>
<div class="infoBox">
<div class="infoBoxContent">
<div class="text">
<span>{{ $t("strings.numberOfUnspentOutputs") }}</span>
</div>
<div class="value">
<span>{{ address.num_unspent_outputs }}</span>
</div>
</div>
</div>
</div>
</template>
<template v-else>
<div class="row justify-between" style="max-width: 768px">
<div class="infoBox">
<div class="infoBoxContent">
<div class="text">
<span>{{ $t("strings.lokiBalance") }}</span>
</div>
<div class="value"><span>N/A</span></div>
</div>
</div>
<div class="infoBox">
<div class="infoBoxContent">
<div class="text">
<span>{{ $t("strings.lokiUnlockedBalance") }}</span>
</div>
<div class="value"><span>N/A</span></div>
</div>
</div>
<div class="infoBox">
<div class="infoBoxContent">
<div class="text">
<span>{{ $t("strings.numberOfUnspentOutputs") }}</span>
</div>
<div class="value"><span>N/A</span></div>
</div>
</div>
</div>
</template>
<div class="q-mt-sm">
<div class="non-selectable">
<q-icon name="history" size="24px" />
<span class="vertical-middle q-ml-xs">{{ $t("strings.recentIncomingTransactionsToAddress") }}</span>
</div>
<div style="margin: 0 -16px;">
<TxList
:key="address.address"
type="all_in"
:limit="5"
:to-incoming-address-index="address.address_index"
/>
</div>
</div>
</template>
</div>
</q-modal-layout>
<template v-if="address != null">
<q-modal v-model="isQRCodeVisible" minimized :content-css="{padding: '25px'}">
<q-modal v-model="isQRCodeVisible" minimized :content-css="{ padding: '25px' }">
<div class="text-center q-mb-sm q-pa-md" style="background: white;">
<QrcodeVue ref="qr" :value="address.address" size="240"> </QrcodeVue>
<q-context-menu>
<q-list link separator style="min-width: 150px; max-height: 300px;">
<q-item v-close-overlay @click.native="copyQR()">
<q-item-main :label="$t('menuItems.copyQR')" />
</q-item>
<q-item v-close-overlay @click.native="saveQR()">
<q-item-main :label="$t('menuItems.saveQR')" />
</q-item>
</q-list>
</q-context-menu>
</div>
<div class="text-center q-mb-sm q-pa-md" style="background: white;">
<qrcode-vue :value="address.address" size="240" ref="qr">
</qrcode-vue>
<q-context-menu>
<q-list link separator style="min-width: 150px; max-height: 300px;">
<q-item v-close-overlay @click.native="copyQR()">
<q-item-main :label="$t('menuItems.copyQR')" />
</q-item>
<q-item v-close-overlay @click.native="saveQR()">
<q-item-main :label="$t('menuItems.saveQR')" />
</q-item>
</q-list>
</q-context-menu>
</div>
<q-btn
color="primary"
@click="isQRCodeVisible = false"
:label="$t('buttons.close')"
/>
</q-modal>
<q-btn color="primary" :label="$t('buttons.close')" @click="isQRCodeVisible = false" />
</q-modal>
</template>
</q-modal>
</q-modal>
</template>
<script>
import { mapState } from "vuex"
const { clipboard, nativeImage } = require("electron")
import AddressHeader from "components/address_header"
import FormatLoki from "components/format_loki"
import { mapState } from "vuex";
const { clipboard, nativeImage } = require("electron");
import AddressHeader from "components/address_header";
import FormatLoki from "components/format_loki";
import QrcodeVue from "qrcode.vue";
import TxList from "components/tx_list"
import TxList from "components/tx_list";
export default {
name: "AddressDetails",
computed: mapState({
addressHeaderInfo (state) {
if (!this.address) return null
name: "AddressDetails",
components: {
AddressHeader,
TxList,
FormatLoki,
QrcodeVue
},
data() {
return {
isVisible: false,
isQRCodeVisible: false,
address: null
};
},
computed: mapState({
addressHeaderInfo() {
if (!this.address) return null;
let title = this.$t('strings.addresses.primaryAddress')
if (this.address.address_index !== 0) {
title = this.$t('strings.addresses.subAddress') + ' (' + this.$t('strings.addresses.subAddressIndex', { index: this.address.address_index}) + ')'
}
let title = this.$t("strings.addresses.primaryAddress");
if (this.address.address_index !== 0) {
title =
this.$t("strings.addresses.subAddress") +
" (" +
this.$t("strings.addresses.subAddressIndex", {
index: this.address.address_index
}) +
")";
}
const extra = this.address.used ? this.$t('strings.userUsedAddress') : this.$t('strings.userNotUsedAddress')
const extra = this.address.used ? this.$t("strings.userUsedAddress") : this.$t("strings.userNotUsedAddress");
return {
title,
extra
}
}
}),
data () {
return {
isVisible: false,
isQRCodeVisible: false,
address: null
}
},
methods: {
copyQR () {
const data = this.$refs.qr.$el.childNodes[0].toDataURL()
const img = nativeImage.createFromDataURL(data)
clipboard.writeImage(img)
this.$q.notify({
type: "positive",
timeout: 1000,
message: this.$t("notification.positive.qrCopied")
})
},
saveQR() {
let img = this.$refs.qr.$el.childNodes[0].toDataURL()
this.$gateway.send("core", "save_png", {img, type: "QR Code"})
},
copyAddress() {
clipboard.writeText(this.address.address)
this.$q.notify({
type: "positive",
timeout: 1000,
message: this.$t("notification.positive.addressCopied")
})
}
},
components: {
AddressHeader,
TxList,
FormatLoki,
QrcodeVue
return {
title,
extra
};
}
}
}),
methods: {
copyQR() {
const data = this.$refs.qr.$el.childNodes[0].toDataURL();
const img = nativeImage.createFromDataURL(data);
clipboard.writeImage(img);
this.$q.notify({
type: "positive",
timeout: 1000,
message: this.$t("notification.positive.qrCopied")
});
},
saveQR() {
let img = this.$refs.qr.$el.childNodes[0].toDataURL();
this.$gateway.send("core", "save_png", { img, type: "QR Code" });
},
copyAddress() {
clipboard.writeText(this.address.address);
this.$q.notify({
type: "positive",
timeout: 1000,
message: this.$t("notification.positive.addressCopied")
});
}
}
};
</script>
<style>
</style>
<style></style>

View File

@ -1,142 +1,136 @@
<template>
<q-item class="address-header">
<q-item class="address-header">
<q-item-main class="self-start">
<q-item-tile sublabel class="title non-selectable">{{ title }}</q-item-tile>
<q-item-tile class="break-all" label>{{ address }}</q-item-tile>
<q-item-tile v-if="payment_id" sublabel>{{ $t("fieldLabels.paymentId") }}: {{ payment_id }}</q-item-tile>
<q-item-tile v-if="extra" sublabel class="extra non-selectable">{{ extra }}</q-item-tile>
<q-item-tile sublabel class="title non-selectable">{{ title }}</q-item-tile>
<q-item-tile class="break-all" label>{{ address }}</q-item-tile>
<q-item-tile v-if="payment_id" sublabel>{{ $t("fieldLabels.paymentId") }}: {{ payment_id }}</q-item-tile>
<q-item-tile v-if="extra" sublabel class="extra non-selectable">{{ extra }}</q-item-tile>
</q-item-main>
<q-item-side v-if="showCopy">
<q-btn
color="primary"
style="width:25px;"
size="sm" icon="file_copy"
ref="copy"
@click="copyAddress">
<q-tooltip anchor="center left" self="center right" :offset="[5, 10]">
{{ $t("menuItems.copyAddress") }}
</q-tooltip>
</q-btn>
<q-btn ref="copy" color="primary" style="width:25px;" size="sm" icon="file_copy" @click="copyAddress">
<q-tooltip anchor="center left" self="center right" :offset="[5, 10]">
{{ $t("menuItems.copyAddress") }}
</q-tooltip>
</q-btn>
</q-item-side>
<q-context-menu>
<q-list link separator style="min-width: 150px; max-height: 300px;">
<q-item v-close-overlay
@click.native="copyAddress($event)">
<q-item-main :label="$t('menuItems.copyAddress')" />
</q-item>
</q-list>
<q-list link separator style="min-width: 150px; max-height: 300px;">
<q-item v-close-overlay @click.native="copyAddress($event)">
<q-item-main :label="$t('menuItems.copyAddress')" />
</q-item>
</q-list>
</q-context-menu>
</q-item>
</q-item>
</template>
<script>
const { clipboard } = require("electron")
import Identicon from "components/identicon"
const { clipboard } = require("electron");
export default {
name: "AddressHeader",
props: {
title: {
type: String,
required: true
},
address: {
type: String,
required: true
},
payment_id: {
type: String,
required: false
},
extra: {
type: String,
required: false
},
showCopy: {
type: Boolean,
required: false,
default: true
}
name: "AddressHeader",
props: {
title: {
type: String,
required: true
},
data () {
return {}
address: {
type: String,
required: true
},
methods: {
copyAddress (event) {
if (event) {
event.stopPropagation()
}
if (this.$refs.copy) {
this.$refs.copy.$el.blur()
}
clipboard.writeText(this.address)
if(this.payment_id) {
this.$q.dialog({
title: this.$t("dialog.copyAddress.title"),
message: this.$t("dialog.copyAddress.message"),
ok: {
label: this.$t(`dialog.${key}.ok`)
},
}).catch(() => null).then(() => {
this.$q.notify({
type: "positive",
timeout: 1000,
message: this.$t("notification.positive.addressCopied")
})
})
} else {
this.$q.notify({
type: "positive",
timeout: 1000,
message: this.$t("notification.positive.addressCopied")
})
}
},
paymentId: {
type: String,
required: false,
default: undefined
},
components: {
Identicon
extra: {
type: String,
required: false,
default: undefined
},
showCopy: {
type: Boolean,
required: false,
default: true
}
}
},
data() {
return {};
},
methods: {
copyAddress(event) {
if (event) {
event.stopPropagation();
}
if (this.$refs.copy) {
this.$refs.copy.$el.blur();
}
clipboard.writeText(this.address);
if (this.payment_id) {
this.$q
.dialog({
title: this.$t("dialog.copyAddress.title"),
message: this.$t("dialog.copyAddress.message"),
ok: {
label: this.$t(`dialog.copyAddress.ok`)
}
})
.catch(() => null)
.then(() => {
this.$q.notify({
type: "positive",
timeout: 1000,
message: this.$t("notification.positive.addressCopied")
});
});
} else {
this.$q.notify({
type: "positive",
timeout: 1000,
message: this.$t("notification.positive.addressCopied")
});
}
}
}
};
</script>
<style lang="scss">
.address-header {
padding: 0;
img {
float:left;
margin-right: 15px;
}
h3 {
margin: 15px 0 0;
}
p {
word-break: break-all;
}
&::after {
content: "";
clear: both;
display: table;
padding: 0;
img {
float: left;
margin-right: 15px;
}
h3 {
margin: 15px 0 0;
}
p {
word-break: break-all;
}
&::after {
content: "";
clear: both;
display: table;
}
.q-item-main {
.q-item-label {
font-weight: 400;
}
.q-item-main {
.q-item-label {
font-weight: 400;
}
.q-item-sublabel, .q-list-header {
font-size: 13px;
}
.title {
font-size: 14px;
margin-bottom: 2px;
}
.extra {
margin-top: 8px;
}
.q-item-sublabel,
.q-list-header {
font-size: 13px;
}
.title {
font-size: 14px;
margin-bottom: 2px;
}
.extra {
margin-top: 8px;
}
}
}
</style>

View File

@ -1,196 +1,200 @@
<template>
<div class="check-transaction">
<div class="check-transaction">
<div class="q-pa-md">
<div class="q-mb-lg description">
{{ $t('strings.checkTransaction.description') }}
<div class="q-mb-lg description">{{ $t("strings.checkTransaction.description") }}</div>
<div>
<LokiField :label="$t('fieldLabels.transactionId')" :error="$v.txid.$error">
<q-input
v-model.trim="txid"
:dark="theme == 'dark'"
:placeholder="$t('placeholders.pasteTransactionId')"
hide-underline
@blur="$v.txid.$touch"
/>
</LokiField>
<LokiField class="q-mt-md" :label="$t('fieldLabels.address')" :error="$v.address.$error" optional>
<q-input
v-model.trim="address"
:dark="theme == 'dark'"
:placeholder="$t('placeholders.recipientWalletAddress')"
hide-underline
@blur="$v.address.$touch"
/>
</LokiField>
<LokiField class="q-mt-md" :label="$t('fieldLabels.message')" optional>
<q-input
v-model.trim="message"
:dark="theme == 'dark'"
:placeholder="$t('placeholders.proveOptionalMessage')"
hide-underline
/>
</LokiField>
<LokiField class="q-mt-md" :label="$t('fieldLabels.signature')" :error="$v.signature.$error">
<q-input
v-model.trim="signature"
:dark="theme == 'dark'"
:placeholder="$t('placeholders.pasteTransactionProof')"
hide-underline
/>
</LokiField>
<q-field class="buttons q-pt-sm">
<q-btn color="primary" :label="$t('buttons.check')" @click="check" />
<q-btn v-if="canClear" color="secondary" :label="$t('buttons.clear')" @click="clear" />
</q-field>
</div>
<div v-if="status.state.txid">
<div class="q-mb-sm">
<div class="title">{{ $t("strings.transactionID") }}</div>
<div>{{ status.state.txid }}</div>
</div>
<div>
<LokiField :label="$t('fieldLabels.transactionId')" :error="$v.txid.$error">
<q-input v-model.trim="txid"
:dark="theme=='dark'"
:placeholder="$t('placeholders.pasteTransactionId')"
@blur="$v.txid.$touch"
hide-underline
/>
</LokiField>
<LokiField class="q-mt-md" :label="$t('fieldLabels.address')" :error="$v.address.$error" optional>
<q-input v-model.trim="address"
:dark="theme=='dark'"
:placeholder="$t('placeholders.recipientWalletAddress')"
@blur="$v.address.$touch"
hide-underline
/>
</LokiField>
<LokiField class="q-mt-md" :label="$t('fieldLabels.message')" optional>
<q-input v-model.trim="message"
:dark="theme=='dark'"
:placeholder="$t('placeholders.proveOptionalMessage')"
hide-underline
/>
</LokiField>
<LokiField class="q-mt-md" :label="$t('fieldLabels.signature')" :error="$v.signature.$error">
<q-input v-model.trim="signature"
:dark="theme=='dark'"
:placeholder="$t('placeholders.pasteTransactionProof')"
hide-underline
/>
</LokiField>
<q-field class="buttons q-pt-sm">
<q-btn color="primary" @click="check" :label="$t('buttons.check')" />
<q-btn color="secondary" @click="clear" :label="$t('buttons.clear')" v-if="canClear"/>
</q-field>
<div class="q-mb-sm">
<div class="title">{{ $t("strings.checkTransaction.infoTitles.validTransaction") }}</div>
<div :class="status.state.good ? 'good' : 'bad'">{{ validTransaction }}</div>
</div>
<div v-if="status.state.txid">
<div class="q-mb-sm">
<div class="title">{{ $t('strings.transactionID') }}</div>
<div>{{ status.state.txid }}</div>
</div>
<div class="q-mb-sm">
<div class="title">{{ $t('strings.checkTransaction.infoTitles.validTransaction') }}</div>
<div :class="status.state.good ? 'good' : 'bad'">{{ validTransaction }}</div>
</div>
<div class="q-mb-sm" v-if="status.state.received != null">
<div class="title">{{ $t('strings.checkTransaction.infoTitles.received') }}</div>
<div><FormatLoki :amount="status.state.received" raw-value /></div>
</div>
<div class="q-mb-sm" v-if="status.state.in_pool != null">
<div class="title">{{ $t('strings.checkTransaction.infoTitles.inPool') }}</div>
<div>{{ status.state.in_pool }}</div>
</div>
<div class="q-mb-sm" v-if="status.state.confirmations != null">
<div class="title">{{ $t('strings.checkTransaction.infoTitles.confirmations') }}</div>
<div>{{ status.state.confirmations }}</div>
</div>
<div v-if="status.state.received != null" class="q-mb-sm">
<div class="title">{{ $t("strings.checkTransaction.infoTitles.received") }}</div>
<div>
<FormatLoki :amount="status.state.received" raw-value />
</div>
</div>
<div v-if="status.state.in_pool != null" class="q-mb-sm">
<div class="title">{{ $t("strings.checkTransaction.infoTitles.inPool") }}</div>
<div>{{ status.state.in_pool }}</div>
</div>
<div v-if="status.state.confirmations != null" class="q-mb-sm">
<div class="title">{{ $t("strings.checkTransaction.infoTitles.confirmations") }}</div>
<div>{{ status.state.confirmations }}</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapState } from "vuex"
import { required } from "vuelidate/lib/validators"
import { address } from "src/validators/common"
import { i18n } from "plugins/i18n"
import LokiField from "components/loki_field"
import FormatLoki from "components/format_loki"
import { mapState } from "vuex";
import { required } from "vuelidate/lib/validators";
import { address } from "src/validators/common";
import { i18n } from "plugins/i18n";
import LokiField from "components/loki_field";
import FormatLoki from "components/format_loki";
export default {
name: "CheckTransaction",
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
status: state => state.gateway.check_transaction_status,
canClear () {
return this.txid !== "" || this.address !== "" || this.message !== "" || this.signature != ""
},
validTransaction () {
let key = this.status.state.good ? "yes" : "no"
return i18n.t(`strings.checkTransaction.validTransaction.${key}`)
}
}),
data() {
return {
txid: "",
address: "",
message: "",
signature: ""
}
name: "CheckTransaction",
components: {
LokiField,
FormatLoki
},
data() {
return {
txid: "",
address: "",
message: "",
signature: ""
};
},
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
status: state => state.gateway.check_transaction_status,
canClear() {
return this.txid !== "" || this.address !== "" || this.message !== "" || this.signature != "";
},
validations: {
txid: { required },
address: {
isAddress(value) {
if (value === '') return true
return new Promise(resolve => {
address(value, this.$gateway)
.then(() => resolve(true))
.catch(e => resolve(false))
});
}
},
signature: { required }
},
watch: {
status: {
handler(val, old){
if(val.code == old.code) return
switch(this.status.code) {
case -1:
this.$q.notify({
type: "negative",
timeout: 3000,
message: this.status.message
})
break;
}
},
deep: true
},
},
methods: {
check() {
this.$v.txid.$touch()
this.$v.address.$touch()
this.$v.signature.$touch()
if (this.$v.txid.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.enterTransactionId")
})
return
}
if (this.$v.signature.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.enterTransactionProof")
})
return
}
if (this.$v.address.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.invalidAddress")
})
return
}
this.$gateway.send("wallet", "check_transaction", {
txid: this.txid,
signature: this.signature,
address: this.address,
message: this.message
})
},
clear() {
this.txid = ""
this.address = ""
this.message = ""
this.signature = ""
this.$v.$reset();
}
},
components: {
LokiField,
FormatLoki
validTransaction() {
let key = this.status.state.good ? "yes" : "no";
return i18n.t(`strings.checkTransaction.validTransaction.${key}`);
}
}
}),
validations: {
txid: { required },
address: {
isAddress(value) {
if (value === "") return true;
return new Promise(resolve => {
address(value, this.$gateway)
.then(() => resolve(true))
.catch(() => resolve(false));
});
}
},
signature: { required }
},
watch: {
status: {
handler(val, old) {
if (val.code == old.code) return;
switch (this.status.code) {
case -1:
this.$q.notify({
type: "negative",
timeout: 3000,
message: this.status.message
});
break;
}
},
deep: true
}
},
methods: {
check() {
this.$v.txid.$touch();
this.$v.address.$touch();
this.$v.signature.$touch();
if (this.$v.txid.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.enterTransactionId")
});
return;
}
if (this.$v.signature.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.enterTransactionProof")
});
return;
}
if (this.$v.address.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.invalidAddress")
});
return;
}
this.$gateway.send("wallet", "check_transaction", {
txid: this.txid,
signature: this.signature,
address: this.address,
message: this.message
});
},
clear() {
this.txid = "";
this.address = "";
this.message = "";
this.signature = "";
this.$v.$reset();
}
}
};
</script>
<style lang="scss">
.check-transaction {
.description {
white-space: pre-line;
}
.buttons {
.q-btn:not(:first-child) {
margin-left: 8px;
}
.description {
white-space: pre-line;
}
.buttons {
.q-btn:not(:first-child) {
margin-left: 8px;
}
}
}
</style>

View File

@ -1,98 +1,87 @@
<template>
<q-layout-footer class="status-footer">
<q-layout-footer class="status-footer">
<div class="status-line row items-center">
<div class="status row items-center">
<span>{{ $t("footer.status") }}:</span>
<span class="status-text" :class="[status]">{{ $t(`footer.${status}`) }}</span>
</div>
<div class="row">
<template v-if="config_daemon.type !== 'remote'">
<div>Daemon: {{ daemon.info.height_without_bootstrap }} / {{ target_height }} ({{ daemon_local_pct }}%)</div>
</template>
<div class="status row items-center">
<span>{{ $t("footer.status") }}:</span>
<span class="status-text" :class="[status]">{{ $t(`footer.${status}`) }}</span>
</div>
<div class="row">
<template v-if="config_daemon.type !== 'remote'">
<div>Daemon: {{ daemon.info.height_without_bootstrap }} / {{ target_height }} ({{ daemon_local_pct }}%)</div>
</template>
<template v-if="config_daemon.type !== 'local'">
<div>{{ $t("footer.remote") }}: {{ daemon.info.height }}</div>
</template>
<div>{{ $t("footer.wallet") }}: {{ wallet.info.height }} / {{ target_height }} ({{ wallet_pct }}%)</div>
</div>
<template v-if="config_daemon.type !== 'local'">
<div>{{ $t("footer.remote") }}: {{ daemon.info.height }}</div>
</template>
<div>{{ $t("footer.wallet") }}: {{ wallet.info.height }} / {{ target_height }} ({{ wallet_pct }}%)</div>
</div>
</div>
<div class="status-bars" :class="[status]">
<div v-bind:style="{ width: daemon_pct+'%' }"></div>
<div v-bind:style="{ width: wallet_pct+'%' }"></div>
<div :style="{ width: daemon_pct + '%' }"></div>
<div :style="{ width: wallet_pct + '%' }"></div>
</div>
</q-layout-footer>
</q-layout-footer>
</template>
<script>
import { mapState } from "vuex"
import { mapState } from "vuex";
export default {
name: "StatusFooter",
computed: mapState({
name: "StatusFooter",
data() {
return {};
},
computed: mapState({
config: state => state.gateway.app.config,
daemon: state => state.gateway.daemon,
wallet: state => state.gateway.wallet,
config: state => state.gateway.app.config,
daemon: state => state.gateway.daemon,
wallet: state => state.gateway.wallet,
config_daemon (state) {
return this.config.daemons[this.config.app.net_type]
},
target_height (state) {
if(this.config_daemon.type === "local")
return Math.max(this.daemon.info.height, this.daemon.info.target_height)
else
return this.daemon.info.height
},
daemon_pct (state) {
if(this.config_daemon.type === "local")
return this.daemon_local_pct
return 0
},
daemon_local_pct (state) {
if(this.config_daemon.type === "remote")
return 0
let pct = (100 * this.daemon.info.height_without_bootstrap / this.target_height).toFixed(1)
if(pct == 100.0 && this.daemon.info.height_without_bootstrap < this.target_height)
return 99.9
else
return pct
},
wallet_pct (state) {
let pct = (100 * this.wallet.info.height / this.target_height).toFixed(1)
if(pct == 100.0 && this.wallet.info.height < this.target_height)
return 99.9
else
return pct
},
status(state) {
if(this.config_daemon.type === "local") {
if(this.daemon.info.height_without_bootstrap < this.target_height) {
return "syncing"
} else if(this.wallet.info.height < this.target_height - 1 && this.wallet.info.height != 0) {
return "scanning"
} else {
return "ready"
}
} else {
if(this.wallet.info.height < this.target_height - 1 && this.wallet.info.height != 0) {
return "scanning"
} else if(this.config_daemon.type === "local_remote" && this.daemon.info.height_without_bootstrap < this.target_height) {
return "syncing"
} else {
return "ready"
}
}
return
}
}),
data () {
return {
}
config_daemon() {
return this.config.daemons[this.config.app.net_type];
},
}
target_height() {
if (this.config_daemon.type === "local") return Math.max(this.daemon.info.height, this.daemon.info.target_height);
else return this.daemon.info.height;
},
daemon_pct() {
if (this.config_daemon.type === "local") return this.daemon_local_pct;
return 0;
},
daemon_local_pct() {
if (this.config_daemon.type === "remote") return 0;
let pct = ((100 * this.daemon.info.height_without_bootstrap) / this.target_height).toFixed(1);
if (pct == 100.0 && this.daemon.info.height_without_bootstrap < this.target_height) return 99.9;
else return pct;
},
wallet_pct() {
let pct = ((100 * this.wallet.info.height) / this.target_height).toFixed(1);
if (pct == 100.0 && this.wallet.info.height < this.target_height) return 99.9;
else return pct;
},
status() {
if (this.config_daemon.type === "local") {
if (this.daemon.info.height_without_bootstrap < this.target_height) {
return "syncing";
} else if (this.wallet.info.height < this.target_height - 1 && this.wallet.info.height != 0) {
return "scanning";
} else {
return "ready";
}
} else {
if (this.wallet.info.height < this.target_height - 1 && this.wallet.info.height != 0) {
return "scanning";
} else if (
this.config_daemon.type === "local_remote" &&
this.daemon.info.height_without_bootstrap < this.target_height
) {
return "syncing";
} else {
return "ready";
}
}
}
})
};
</script>
<style lang="scss">
</style>
<style lang="scss"></style>

View File

@ -1,38 +1,34 @@
<template>
<span>
{{ value }} LOKI
</span>
<span> {{ value }} LOKI </span>
</template>
<script>
export default {
name: "FormatLoki",
props: {
amount: {
required: true
},
round: {
type: Boolean,
required: false,
default: false
},
rawValue: {
type: Boolean,
required: false,
default: false
}
name: "FormatLoki",
props: {
amount: {
type: Number,
required: true
},
computed: {
value () {
let value = this.amount / 1e9
if(this.round)
value = value.toFixed(3)
return this.rawValue ? value : value.toLocaleString()
}
round: {
type: Boolean,
required: false,
default: false
},
rawValue: {
type: Boolean,
required: false,
default: false
}
}
},
computed: {
value() {
let value = this.amount / 1e9;
if (this.round) value = value.toFixed(3);
return this.rawValue ? value : value.toLocaleString();
}
}
};
</script>
<style>
</style>
<style></style>

View File

@ -1,225 +1,226 @@
<template>
<div class="identicon"
v-bind:style="{backgroundImage: 'url('+img+')', width: 8*size+'px', height: 8*size+'px'}">
<div
class="identicon"
:style="{
backgroundImage: 'url(' + img + ')',
width: 8 * size + 'px',
height: 8 * size + 'px'
}"
>
<q-context-menu v-if="menu">
<q-list link separator style="min-width: 150px; max-height: 300px;">
<q-item v-close-overlay
:disabled="img == defaultImg"
@click.native="saveIdenticon()">
<q-item-main label="Save identicon to file" />
</q-item>
</q-list>
<q-list link separator style="min-width: 150px; max-height: 300px;">
<q-item v-close-overlay :disabled="img == defaultImg" @click.native="saveIdenticon()">
<q-item-main label="Save identicon to file" />
</q-item>
</q-list>
</q-context-menu>
</div>
</div>
</template>
<script>
export default {
name: "Identicon",
props: {
address: {
default: ""
},
size: {
type: Number,
default: 5
},
menu: {
type: Boolean,
default: false
}
name: "Identicon",
props: {
address: {
type: String,
default: ""
},
data () {
return {
randseed: new Array(4),
img: "",
defaultImg: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYAAACM/rhtAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gkHECkpHU3uFgAAAZlJREFUWMPt2D1PwkAYB/D/AxWIbZUCAvGNwZiAcTG+JQ4sJn4AV7+UfgNXjY6ObrppXBiEGBMUgimKKHDy2jpUUHGTIhe5Z7rc8OTX9p67e0oXu3cmOA4HOA8BFEABFMBhB0q9JnBrEgIxGeqsGx7fCBwSoVExUMpUoV+VwPTG4ICRLQ2BmPJj3qU44Y/K8EdlpE8LeExUBgPU5kZhGiaYXkfmrIhytg6SgMn1cYSWVRARIps+ML3+6zfZE5Dl60gd5b/NmU0ge/6CQpJhYScMAPBFZTC9+PdF0o37GtViE6ZhXZQ8Xom/KiYCQNa4+WbwB5yOe0FkCQspxhcwtKJiYtGq7ucbhtd0dXD7YHeEV1VMbXgBAOVcDbcnT/ycJMElpYOrPNSQPNR5OupMzMS1zvZzfaDbktU2YHht7HP7OdZte2zbgHLI3Rm3aiZ/QHJ+fGjD3i7WNqDTZaUiB/F13WqHHRXbV6BHk0BOAhHA8g3+gPPbQbgUayFe7t3ztwbNVn9+8fz/nqQdif2caDsFUAAFUACHEfgOXvt3FLbL3AsAAAAASUVORK5CYII="
}
size: {
type: Number,
default: 5
},
computed: {
isDefault: function () {
return this.img == this.defaultImg
}
},
created() {
if(this.address && this.isAddressValid(this.address)) {
this.createIcon({
seed: this.address,
scale: 12
})
} else {
this.img = this.defaultImg
}
},
watch: {
address: function(address) {
if(address && this.isAddressValid(address)) {
this.createIcon({
seed: address,
scale: this.size
})
} else {
this.img = this.defaultImg
}
}
},
methods: {
saveIdenticon() {
if(this.img == this.defaultImg)
return
this.$gateway.send("core", "save_png", {img: this.img, type: "Identicon"})
},
isAddressValid(input) {
if(!(/^[0-9A-Za-z]+$/.test(input))) return false
switch (input.substring(0,4)) {
case "Sumo":
case "RYoL":
case "Suto":
case "RYoT":
return input.length === 99
case "Subo":
case "Suso":
return input.length == 98
case "RYoS":
case "RYoU":
return input.length == 99
case "Sumi":
case "RYoN":
case "Suti":
case "RYoE":
return input.length === 110
case "RYoK":
case "RYoH":
return input.length === 55
default:
return false
}
},
seedrand(seed) {
for (var i = 0; i < this.randseed.length; i++) {
this.randseed[i] = 0;
}
for (var i = 0; i < seed.length; i++) {
this.randseed[i%4] = ((this.randseed[i%4] << 5) - this.randseed[i%4]) + seed.charCodeAt(i);
}
},
rand() {
// based on Java's String.hashCode(), expanded to 4 32bit values
var t = this.randseed[0] ^ (this.randseed[0] << 11);
this.randseed[0] = this.randseed[1];
this.randseed[1] = this.randseed[2];
this.randseed[2] = this.randseed[3];
this.randseed[3] = (this.randseed[3] ^ (this.randseed[3] >> 19) ^ t ^ (t >> 8));
return (this.randseed[3]>>>0) / ((1 << 31)>>>0);
},
createColor() {
//saturation is the whole color spectrum
var h = Math.floor(this.rand() * 360);
//saturation goes from 40 to 100, it avoids greyish colors
var s = ((this.rand() * 60) + 40) + '%';
//lightness can be anything from 0 to 100, but probabilities are a bell curve around 50%
var l = ((this.rand()+this.rand()+this.rand()+this.rand()) * 25) + '%';
var color = 'hsl(' + h + ',' + s + ',' + l + ')';
return color;
},
createImageData(size) {
var width = size; // Only support square icons for now
var height = size;
var dataWidth = Math.ceil(width / 2);
var mirrorWidth = width - dataWidth;
var data = [];
for(var y = 0; y < height; y++) {
var row = [];
for(var x = 0; x < dataWidth; x++) {
// this makes foreground and background color to have a 43% (1/2.3) probability
// spot color has 13% chance
row[x] = Math.floor(this.rand()*2.3);
}
var r = row.slice(0, mirrorWidth);
r.reverse();
row = row.concat(r);
for(var i = 0; i < row.length; i++) {
data.push(row[i]);
}
}
return data;
},
buildOpts(opts) {
var newOpts = {};
newOpts.seed = opts.seed || Math.floor((Math.random()*Math.pow(10,16))).toString(16);
this.seedrand(newOpts.seed);
newOpts.size = opts.size || 8;
newOpts.scale = opts.scale || 4;
newOpts.color = opts.color || this.createColor();
newOpts.bgcolor = opts.bgcolor || this.createColor();
newOpts.spotcolor = opts.spotcolor || this.createColor();
return newOpts;
},
renderIcon(opts, canvas) {
opts = this.buildOpts(opts || {});
var imageData = this.createImageData(opts.size);
var width = Math.sqrt(imageData.length);
canvas.width = canvas.height = opts.size * opts.scale;
var cc = canvas.getContext('2d');
cc.fillStyle = opts.bgcolor;
cc.fillRect(0, 0, canvas.width, canvas.height);
cc.fillStyle = opts.color;
for(var i = 0; i < imageData.length; i++) {
// if data is 0, leave the background
if(imageData[i]) {
var row = Math.floor(i / width);
var col = i % width;
// if data is 2, choose spot color, if 1 choose foreground
cc.fillStyle = (imageData[i] == 1) ? opts.color : opts.spotcolor;
cc.fillRect(col * opts.scale, row * opts.scale, opts.scale, opts.scale);
}
}
return canvas;
},
createIcon(opts) {
var canvas = document.createElement('canvas');
this.renderIcon(opts, canvas);
this.img = canvas.toDataURL()
}
menu: {
type: Boolean,
default: false
}
}
},
data() {
return {
randseed: new Array(4),
img: "",
defaultImg:
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYAAACM/rhtAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gkHECkpHU3uFgAAAZlJREFUWMPt2D1PwkAYB/D/AxWIbZUCAvGNwZiAcTG+JQ4sJn4AV7+UfgNXjY6ObrppXBiEGBMUgimKKHDy2jpUUHGTIhe5Z7rc8OTX9p67e0oXu3cmOA4HOA8BFEABFMBhB0q9JnBrEgIxGeqsGx7fCBwSoVExUMpUoV+VwPTG4ICRLQ2BmPJj3qU44Y/K8EdlpE8LeExUBgPU5kZhGiaYXkfmrIhytg6SgMn1cYSWVRARIps+ML3+6zfZE5Dl60gd5b/NmU0ge/6CQpJhYScMAPBFZTC9+PdF0o37GtViE6ZhXZQ8Xom/KiYCQNa4+WbwB5yOe0FkCQspxhcwtKJiYtGq7ucbhtd0dXD7YHeEV1VMbXgBAOVcDbcnT/ycJMElpYOrPNSQPNR5OupMzMS1zvZzfaDbktU2YHht7HP7OdZte2zbgHLI3Rm3aiZ/QHJ+fGjD3i7WNqDTZaUiB/F13WqHHRXbV6BHk0BOAhHA8g3+gPPbQbgUayFe7t3ztwbNVn9+8fz/nqQdif2caDsFUAAFUACHEfgOXvt3FLbL3AsAAAAASUVORK5CYII="
};
},
computed: {
isDefault: function() {
return this.img == this.defaultImg;
}
},
watch: {
address: function(address) {
if (address && this.isAddressValid(address)) {
this.createIcon({
seed: address,
scale: this.size
});
} else {
this.img = this.defaultImg;
}
}
},
created() {
if (this.address && this.isAddressValid(this.address)) {
this.createIcon({
seed: this.address,
scale: 12
});
} else {
this.img = this.defaultImg;
}
},
methods: {
saveIdenticon() {
if (this.img == this.defaultImg) return;
this.$gateway.send("core", "save_png", {
img: this.img,
type: "Identicon"
});
},
isAddressValid(input) {
if (!/^[0-9A-Za-z]+$/.test(input)) return false;
switch (input.substring(0, 4)) {
case "Sumo":
case "RYoL":
case "Suto":
case "RYoT":
return input.length === 99;
case "Subo":
case "Suso":
return input.length == 98;
case "RYoS":
case "RYoU":
return input.length == 99;
case "Sumi":
case "RYoN":
case "Suti":
case "RYoE":
return input.length === 110;
case "RYoK":
case "RYoH":
return input.length === 55;
default:
return false;
}
},
seedrand(seed) {
for (var i = 0; i < this.randseed.length; i++) {
this.randseed[i] = 0;
}
for (var j = 0; j < seed.length; j++) {
this.randseed[j % 4] = (this.randseed[j % 4] << 5) - this.randseed[j % 4] + seed.charCodeAt(j);
}
},
rand() {
// based on Java's String.hashCode(), expanded to 4 32bit values
var t = this.randseed[0] ^ (this.randseed[0] << 11);
this.randseed[0] = this.randseed[1];
this.randseed[1] = this.randseed[2];
this.randseed[2] = this.randseed[3];
this.randseed[3] = this.randseed[3] ^ (this.randseed[3] >> 19) ^ t ^ (t >> 8);
return (this.randseed[3] >>> 0) / ((1 << 31) >>> 0);
},
createColor() {
//saturation is the whole color spectrum
var h = Math.floor(this.rand() * 360);
//saturation goes from 40 to 100, it avoids greyish colors
var s = this.rand() * 60 + 40 + "%";
//lightness can be anything from 0 to 100, but probabilities are a bell curve around 50%
var l = (this.rand() + this.rand() + this.rand() + this.rand()) * 25 + "%";
var color = "hsl(" + h + "," + s + "," + l + ")";
return color;
},
createImageData(size) {
var width = size; // Only support square icons for now
var height = size;
var dataWidth = Math.ceil(width / 2);
var mirrorWidth = width - dataWidth;
var data = [];
for (var y = 0; y < height; y++) {
var row = [];
for (var x = 0; x < dataWidth; x++) {
// this makes foreground and background color to have a 43% (1/2.3) probability
// spot color has 13% chance
row[x] = Math.floor(this.rand() * 2.3);
}
var r = row.slice(0, mirrorWidth);
r.reverse();
row = row.concat(r);
for (var i = 0; i < row.length; i++) {
data.push(row[i]);
}
}
return data;
},
buildOpts(opts) {
var newOpts = {};
newOpts.seed = opts.seed || Math.floor(Math.random() * Math.pow(10, 16)).toString(16);
this.seedrand(newOpts.seed);
newOpts.size = opts.size || 8;
newOpts.scale = opts.scale || 4;
newOpts.color = opts.color || this.createColor();
newOpts.bgcolor = opts.bgcolor || this.createColor();
newOpts.spotcolor = opts.spotcolor || this.createColor();
return newOpts;
},
renderIcon(opts, canvas) {
opts = this.buildOpts(opts || {});
var imageData = this.createImageData(opts.size);
var width = Math.sqrt(imageData.length);
canvas.width = canvas.height = opts.size * opts.scale;
var cc = canvas.getContext("2d");
cc.fillStyle = opts.bgcolor;
cc.fillRect(0, 0, canvas.width, canvas.height);
cc.fillStyle = opts.color;
for (var i = 0; i < imageData.length; i++) {
// if data is 0, leave the background
if (imageData[i]) {
var row = Math.floor(i / width);
var col = i % width;
// if data is 2, choose spot color, if 1 choose foreground
cc.fillStyle = imageData[i] == 1 ? opts.color : opts.spotcolor;
cc.fillRect(col * opts.scale, row * opts.scale, opts.scale, opts.scale);
}
}
return canvas;
},
createIcon(opts) {
var canvas = document.createElement("canvas");
this.renderIcon(opts, canvas);
this.img = canvas.toDataURL();
}
}
};
</script>
<style>
</style>
<style></style>

View File

@ -1,56 +1,56 @@
<template>
<div class="language-select column items-center justify-center">
<div class="language-select column items-center justify-center">
<h6 class="q-my-md" style="font-weight: 300">{{ $t("strings.selectLanguage") }}:</h6>
<div class="row justify-center">
<q-btn
class="row justify-center items-center"
v-for="option in options"
:key="option.value"
:color="lang === option.value ? 'primary' : 'secondary'"
@click="setLanguage(option.value)"
size="md"
>
<span :class="`flag-icon flag-icon-${option.flag}`" />
<span>{{ option.label }}</span>
</q-btn>
<q-btn
v-for="option in options"
:key="option.value"
class="row justify-center items-center"
:color="lang === option.value ? 'primary' : 'secondary'"
size="md"
@click="setLanguage(option.value)"
>
<span :class="`flag-icon flag-icon-${option.flag}`" />
<span>{{ option.label }}</span>
</q-btn>
</div>
</div>
</div>
</template>
<script>
import { languages } from "src/i18n"
import { languages } from "src/i18n";
export default {
name: "LanguageSelect",
computed: {
lang () {
return this.$i18n.locale
},
options () {
return languages.map(lang => ({
label: lang.name,
value: lang.code,
flag: lang.flag
}))
}
name: "LanguageSelect",
computed: {
lang() {
return this.$i18n.locale;
},
methods: {
setLanguage(lang) {
this.$gateway.send("core", "set_language", { lang })
this.$emit('select', lang)
}
},
}
options() {
return languages.map(lang => ({
label: lang.name,
value: lang.code,
flag: lang.flag
}));
}
},
methods: {
setLanguage(lang) {
this.$gateway.send("core", "set_language", { lang });
this.$emit("select", lang);
}
}
};
</script>
<style lang="scss">
.language-select {
.q-btn {
margin: 2px;
}
.q-btn {
margin: 2px;
}
.flag-icon {
margin-right: 4px;
}
.flag-icon {
margin-right: 4px;
}
}
</style>

View File

@ -1,94 +1,99 @@
<template>
<div class="loki-field" :class="{disable, 'disable-hover': disableHover}">
<div class="label row items-center" v-if="label" :disabled="disable">
{{ label }}
<span v-if="optional" class="optional">({{ $t("fieldLabels.optional") }})</span>
<div class="loki-field" :class="{ disable, 'disable-hover': disableHover }">
<div v-if="label" class="label row items-center" :disabled="disable">
{{ label }}
<span v-if="optional" class="optional">({{ $t("fieldLabels.optional") }})</span>
</div>
<div class="content row items-center" :class="{error}">
<slot></slot>
<div class="content row items-center" :class="{ error }">
<slot></slot>
</div>
<div class="error-label" v-if="error && errorLabel" :disabled="disable">{{ errorLabel }}</div>
</div>
<div v-if="error && errorLabel" class="error-label" :disabled="disable">
{{ errorLabel }}
</div>
</div>
</template>
<script>
export default {
name: "LokiField",
props: {
label: {
type: String,
required: false
},
error: {
type: Boolean,
required: false,
default: false
},
errorLabel: {
type: String,
required: false
},
optional: {
type: Boolean,
required: false,
default: false
},
disable: {
type: Boolean,
required: false,
default: false
},
disableHover: {
type: Boolean,
required: false,
default: false
}
name: "LokiField",
props: {
label: {
type: String,
required: false,
default: undefined
},
data () {
return {
}
error: {
type: Boolean,
required: false,
default: false
},
}
errorLabel: {
type: String,
required: false,
default: undefined
},
optional: {
type: Boolean,
required: false,
default: false
},
disable: {
type: Boolean,
required: false,
default: false
},
disableHover: {
type: Boolean,
required: false,
default: false
}
},
data() {
return {};
}
};
</script>
<style lang="scss">
.loki-field {
.label {
margin: 6px 0;
font-weight: bold;
font-size: 12px;
.label {
margin: 6px 0;
font-weight: bold;
font-size: 12px;
// Disable text selection
-webkit-user-select: none;
user-select: none;
cursor: default;
// Disable text selection
-webkit-user-select: none;
user-select: none;
cursor: default;
.optional {
font-weight: 400;
margin-left: 4px;
}
.optional {
font-weight: 400;
margin-left: 4px;
}
.content {
border-radius: 3px;
padding: 6px 8px;
min-height: 46px;
}
.content {
border-radius: 3px;
padding: 6px 8px;
min-height: 46px;
> * {
margin-right: 4px;
}
> *:last-child {
margin-right: 0px;
}
.q-input, .q-select, .q-datetime-input {
flex: 1;
}
.q-btn {
padding: 4px 8px;
font-size: 12px !important;
}
> * {
margin-right: 4px;
}
> *:last-child {
margin-right: 0px;
}
.q-input,
.q-select,
.q-datetime-input {
flex: 1;
}
.q-btn {
padding: 4px 8px;
font-size: 12px !important;
}
}
}
</style>

View File

@ -1,170 +1,160 @@
<template>
<div>
<div>
<q-btn class="menu" icon="menu" size="md" flat>
<q-popover>
<q-list separator link>
<q-item v-close-overlay @click.native="switchWallet" v-if="!disableSwitchWallet">
<q-item-main>
<q-item-tile label>{{ $t("menuItems.switchWallet") }}</q-item-tile>
</q-item-main>
</q-item>
<q-item v-close-overlay @click.native="openSettings">
<q-item-main>
<q-item-tile label>{{ $t("menuItems.settings") }}</q-item-tile>
</q-item-main>
</q-item>
<q-item v-close-overlay @click.native="showAbout(true)">
<q-item-main>
<q-item-tile label>{{ $t("menuItems.about") }}</q-item-tile>
</q-item-main>
</q-item>
<q-item v-close-overlay @click.native="exit">
<q-item-main>
<q-item-tile label>{{ $t("menuItems.exit") }}</q-item-tile>
</q-item-main>
</q-item>
</q-list>
</q-popover>
<q-popover>
<q-list separator link>
<q-item v-if="!disableSwitchWallet" v-close-overlay @click.native="switchWallet">
<q-item-main>
<q-item-tile label>{{ $t("menuItems.switchWallet") }}</q-item-tile>
</q-item-main>
</q-item>
<q-item v-close-overlay @click.native="openSettings">
<q-item-main>
<q-item-tile label>{{ $t("menuItems.settings") }}</q-item-tile>
</q-item-main>
</q-item>
<q-item v-close-overlay @click.native="showAbout(true)">
<q-item-main>
<q-item-tile label>{{ $t("menuItems.about") }}</q-item-tile>
</q-item-main>
</q-item>
<q-item v-close-overlay @click.native="exit">
<q-item-main>
<q-item-tile label>{{ $t("menuItems.exit") }}</q-item-tile>
</q-item-main>
</q-item>
</q-list>
</q-popover>
</q-btn>
<settings-modal ref="settingsModal" />
<q-modal minimized ref="aboutModal">
<div class="about-modal">
<SettingsModal ref="settingsModal" />
<q-modal ref="aboutModal" minimized>
<div class="about-modal">
<img class="q-mb-md" src="statics/loki.svg" height="42" />
<img class="q-mb-md" src="statics/loki.svg" height="42" />
<p class="q-my-sm">Wallet Version: v{{ version }}</p>
<p class="q-my-sm">Deaemon Version: v{{ daemonVersion }}</p>
<p class="q-my-sm">Copyright (c) 2018-2019, Loki Project</p>
<p class="q-my-sm">Copyright (c) 2018, Ryo Currency Project</p>
<p class="q-my-sm">All rights reserved.</p>
<p class="q-my-sm">Wallet Version: v{{version}}</p>
<p class="q-my-sm">Deaemon Version: v{{daemonVersion}}</p>
<p class="q-my-sm">Copyright (c) 2018-2019, Loki Project</p>
<p class="q-my-sm">Copyright (c) 2018, Ryo Currency Project</p>
<p class="q-my-sm">All rights reserved.</p>
<div class="q-mt-md q-mb-lg external-links">
<p>
<a @click="openExternal('https://loki.network/')" href="#">https://loki.network/</a>
</p>
<p>
<a @click="openExternal('https://t.me/joinchat/DeNvR0JJ4JPn6TVSQjCsZQ')" href="#">Telegram</a> -
<a @click="openExternal('https://discordapp.com/invite/67GXfD6')" href="#">Discord</a> -
<a @click="openExternal('https://www.reddit.com/r/LokiProject/')" href="#">Reddit</a> -
<a @click="openExternal('https://github.com/loki-project/loki-electron-wallet')" href="#">Github</a>
</p>
</div>
<q-btn
color="primary"
@click="showAbout(false)"
label="Close"
/>
<div class="q-mt-md q-mb-lg external-links">
<p>
<a href="#" @click="openExternal('https://loki.network/')">https://loki.network/</a>
</p>
<p>
<a href="#" @click="openExternal('https://t.me/joinchat/DeNvR0JJ4JPn6TVSQjCsZQ')">Telegram</a>
-
<a href="#" @click="openExternal('https://discordapp.com/invite/67GXfD6')">Discord</a>
-
<a href="#" @click="openExternal('https://www.reddit.com/r/LokiProject/')">Reddit</a>
-
<a href="#" @click="openExternal('https://github.com/loki-project/loki-electron-wallet')">Github</a>
</p>
</div>
<q-btn color="primary" label="Close" @click="showAbout(false)" />
</div>
</q-modal>
</div>
</div>
</template>
<script>
import { version } from "../../package.json"
import { mapState } from "vuex"
import SettingsModal from "components/settings"
import { version } from "../../package.json";
import { mapState } from "vuex";
import SettingsModal from "components/settings";
export default {
name: "MainMenu",
props: {
disableSwitchWallet: {
type: Boolean,
required: false,
default: false
}
},
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
isRPCSyncing: state => state.gateway.wallet.isRPCSyncing,
daemon: state => state.gateway.daemon,
daemonVersion (state) {
return this.daemon.info.version || 'N/A'
}
}),
data() {
return {
version: "",
}
},
mounted () {
this.version = version
},
methods: {
openExternal (url) {
this.$gateway.send("core", "open_url", {url})
},
showAbout (toggle) {
if(toggle)
this.$refs.aboutModal.show()
else
this.$refs.aboutModal.hide()
},
openSettings () {
this.$refs.settingsModal.isVisible = true
},
switchWallet () {
// If the rpc is syncing then we want to tell the user to restart
if (this.isRPCSyncing) {
this.$gateway.confirmClose(this.$t("dialog.switchWallet.restartMessage"), true)
return
}
// Allow switching normally because rpc won't be blocked
this.$q.dialog({
title: this.$t("dialog.switchWallet.title"),
message: this.$t("dialog.switchWallet.closeMessage"),
ok: {
label: this.$t("dialog.buttons.ok")
},
cancel: {
flat: true,
label: this.$t("dialog.buttons.cancel"),
color: this.theme=="dark"?"white":"dark"
}
}).then(() => {
this.$router.replace({ path: "/wallet-select" })
this.$gateway.send("wallet", "close_wallet")
setTimeout(() => {
// short delay to prevent wallet data reaching the
// websocket moments after we close and reset data
this.$store.dispatch("gateway/resetWalletData")
}, 250);
}).catch(() => {
})
},
exit () {
this.$gateway.confirmClose(this.$t("dialog.exit.message"))
}
},
components: {
SettingsModal
name: "MainMenu",
components: {
SettingsModal
},
props: {
disableSwitchWallet: {
type: Boolean,
required: false,
default: false
}
}
},
data() {
return {
version: ""
};
},
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
isRPCSyncing: state => state.gateway.wallet.isRPCSyncing,
daemon: state => state.gateway.daemon,
daemonVersion() {
return this.daemon.info.version || "N/A";
}
}),
mounted() {
this.version = version;
},
methods: {
openExternal(url) {
this.$gateway.send("core", "open_url", { url });
},
showAbout(toggle) {
if (toggle) this.$refs.aboutModal.show();
else this.$refs.aboutModal.hide();
},
openSettings() {
this.$refs.settingsModal.isVisible = true;
},
switchWallet() {
// If the rpc is syncing then we want to tell the user to restart
if (this.isRPCSyncing) {
this.$gateway.confirmClose(this.$t("dialog.switchWallet.restartMessage"), true);
return;
}
// Allow switching normally because rpc won't be blocked
this.$q
.dialog({
title: this.$t("dialog.switchWallet.title"),
message: this.$t("dialog.switchWallet.closeMessage"),
ok: {
label: this.$t("dialog.buttons.ok")
},
cancel: {
flat: true,
label: this.$t("dialog.buttons.cancel"),
color: this.theme == "dark" ? "white" : "dark"
}
})
.then(() => {
this.$router.replace({ path: "/wallet-select" });
this.$gateway.send("wallet", "close_wallet");
setTimeout(() => {
// short delay to prevent wallet data reaching the
// websocket moments after we close and reset data
this.$store.dispatch("gateway/resetWalletData");
}, 250);
})
.catch(() => {});
},
exit() {
this.$gateway.confirmClose(this.$t("dialog.exit.message"));
}
}
};
</script>
<style lang="scss">
.about-modal {
padding: 25px;
padding: 25px;
.external-links {
a {
color: #497dc6;
text-decoration: none;
&:hover,
&:active,
&:visited {
text-decoration: underline;
}
}
.external-links {
a {
color: #497dc6;
text-decoration: none;
&:hover,
&:active,
&:visited {
text-decoration: underline;
}
}
}
}
</style>

View File

@ -1,174 +1,176 @@
<template>
<div class="prove-transaction">
<div class="prove-transaction">
<div class="q-pa-md">
<div class="q-mb-lg description">
{{ $t('strings.proveTransactionDescription') }}
</div>
<div>
<LokiField :label="$t('fieldLabels.transactionId')" :error="$v.txid.$error">
<q-input v-model.trim="txid"
:dark="theme=='dark'"
:placeholder="$t('placeholders.pasteTransactionId')"
@blur="$v.txid.$touch"
hide-underline
/>
</LokiField>
<LokiField class="q-mt-md" :label="$t('fieldLabels.address')" :error="$v.address.$error" optional>
<q-input v-model.trim="address"
:dark="theme=='dark'"
:placeholder="$t('placeholders.recipientWalletAddress')"
@blur="$v.address.$touch"
hide-underline
/>
</LokiField>
<LokiField class="q-mt-md" :label="$t('fieldLabels.message')" optional>
<q-input v-model.trim="message"
:dark="theme=='dark'"
:placeholder="$t('placeholders.proveOptionalMessage')"
hide-underline
/>
</LokiField>
<q-field class="buttons q-pt-sm">
<q-btn color="primary" @click="generate" :label="$t('buttons.generate')" />
<q-btn color="secondary" @click="clear" :label="$t('buttons.clear')" v-if="canClear"/>
<q-btn color="secondary" @click="copy" :label="$t('buttons.copySignature')" v-if="status.state.signature"/>
</q-field>
</div>
<div v-if="status.state.signature">
<div class="txid q-mb-sm">
<div class="title">{{ $t('strings.transactionID') }}</div>
<div>{{ status.state.txid }}</div>
</div>
<p class="signature">
{{ status.state.signature }}
</p>
<div class="q-mb-lg description">
{{ $t("strings.proveTransactionDescription") }}
</div>
<div>
<LokiField :label="$t('fieldLabels.transactionId')" :error="$v.txid.$error">
<q-input
v-model.trim="txid"
:dark="theme == 'dark'"
:placeholder="$t('placeholders.pasteTransactionId')"
hide-underline
@blur="$v.txid.$touch"
/>
</LokiField>
<LokiField class="q-mt-md" :label="$t('fieldLabels.address')" :error="$v.address.$error" optional>
<q-input
v-model.trim="address"
:dark="theme == 'dark'"
:placeholder="$t('placeholders.recipientWalletAddress')"
hide-underline
@blur="$v.address.$touch"
/>
</LokiField>
<LokiField class="q-mt-md" :label="$t('fieldLabels.message')" optional>
<q-input
v-model.trim="message"
:dark="theme == 'dark'"
:placeholder="$t('placeholders.proveOptionalMessage')"
hide-underline
/>
</LokiField>
<q-field class="buttons q-pt-sm">
<q-btn color="primary" :label="$t('buttons.generate')" @click="generate" />
<q-btn v-if="canClear" color="secondary" :label="$t('buttons.clear')" @click="clear" />
<q-btn v-if="status.state.signature" color="secondary" :label="$t('buttons.copySignature')" @click="copy" />
</q-field>
</div>
<div v-if="status.state.signature">
<div class="txid q-mb-sm">
<div class="title">{{ $t("strings.transactionID") }}</div>
<div>{{ status.state.txid }}</div>
</div>
<p class="signature">
{{ status.state.signature }}
</p>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapState } from "vuex"
import { required } from "vuelidate/lib/validators"
import { address } from "src/validators/common"
import LokiField from "components/loki_field"
import { clipboard } from 'electron';
import { mapState } from "vuex";
import { required } from "vuelidate/lib/validators";
import { address } from "src/validators/common";
import LokiField from "components/loki_field";
import { clipboard } from "electron";
export default {
name: "ProveTransaction",
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
status: state => state.gateway.prove_transaction_status,
canClear () {
return this.txid !== "" || this.address !== "" || this.message !== ""
}
}),
data() {
return {
txid: "",
address: "",
message: "",
}
},
validations: {
txid: { required },
address: {
isAddress(value) {
if (value === '') return true
return new Promise(resolve => {
address(value, this.$gateway)
.then(() => resolve(true))
.catch(e => resolve(false))
});
}
}
},
watch: {
status: {
handler(val, old){
if(val.code == old.code) return
switch(this.status.code) {
case -1:
this.$q.notify({
type: "negative",
timeout: 3000,
message: this.status.message
})
break;
}
},
deep: true
},
},
methods: {
generate() {
this.$v.txid.$touch()
this.$v.address.$touch()
if (this.$v.txid.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.enterTransactionId")
})
return
}
if (this.$v.address.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.invalidAddress")
})
return
}
this.$gateway.send("wallet", "prove_transaction", {
txid: this.txid.trim(),
address: this.address.trim(),
message: this.message.trim()
})
},
clear() {
this.txid = ""
this.address = ""
this.message = ""
this.$v.$reset();
},
copy() {
clipboard.writeText(this.status.state.signature)
this.$q.notify({
type: "positive",
timeout: 1000,
message: this.$t("notification.positive.signatureCopied")
})
}
},
components: {
LokiField
name: "ProveTransaction",
components: {
LokiField
},
data() {
return {
txid: "",
address: "",
message: ""
};
},
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
status: state => state.gateway.prove_transaction_status,
canClear() {
return this.txid !== "" || this.address !== "" || this.message !== "";
}
}
</script>
}),
validations: {
txid: { required },
address: {
isAddress(value) {
if (value === "") return true;
return new Promise(resolve => {
address(value, this.$gateway)
.then(() => resolve(true))
.catch(() => resolve(false));
});
}
}
},
watch: {
status: {
handler(val, old) {
if (val.code == old.code) return;
switch (this.status.code) {
case -1:
this.$q.notify({
type: "negative",
timeout: 3000,
message: this.status.message
});
break;
}
},
deep: true
}
},
methods: {
generate() {
this.$v.txid.$touch();
this.$v.address.$touch();
if (this.$v.txid.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.enterTransactionId")
});
return;
}
if (this.$v.address.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.invalidAddress")
});
return;
}
this.$gateway.send("wallet", "prove_transaction", {
txid: this.txid.trim(),
address: this.address.trim(),
message: this.message.trim()
});
},
clear() {
this.txid = "";
this.address = "";
this.message = "";
this.$v.$reset();
},
copy() {
clipboard.writeText(this.status.state.signature);
this.$q.notify({
type: "positive",
timeout: 1000,
message: this.$t("notification.positive.signatureCopied")
});
}
}
};
</script>
<style lang="scss">
.prove-transaction {
.description {
white-space: pre-line;
}
.buttons {
.q-btn:not(:first-child) {
margin-left: 8px;
}
}
.signature {
flex: 1;
word-break: break-all;
word-wrap: break-word;
-webkit-user-select: all;
user-select: all;
padding: 8px;
.description {
white-space: pre-line;
}
.buttons {
.q-btn:not(:first-child) {
margin-left: 8px;
}
}
.signature {
flex: 1;
word-break: break-all;
word-wrap: break-word;
-webkit-user-select: all;
user-select: all;
padding: 8px;
}
}
</style>

View File

@ -1,125 +1,112 @@
<template>
<q-list class="loki-list-item" no-border @click.native="details(address)">
<q-list class="loki-list-item" no-border @click.native="details(address)">
<q-item>
<q-item-main>
<q-item-tile class="ellipsis" label>{{ address.address }}</q-item-tile>
<q-item-tile v-if="sublabel" sublabel class="non-selectable">{{ sublabel }}</q-item-tile>
</q-item-main>
<q-item-side>
<q-btn
flat
style="width:25px;"
size="md"
@click="showQR(address.address, $event)"
>
<img :src="qrImage" height="20" />
<q-tooltip anchor="bottom right" self="top right" :offset="[0, 5]">
{{ $t("menuItems.showQRCode") }}
</q-tooltip>
</q-btn>
<q-btn
flat
style="width:25px;"
size="md" icon="file_copy"
@click="copyAddress(address.address, $event)"
>
<q-tooltip anchor="bottom right" self="top right" :offset="[0, 5]">
{{ $t("menuItems.copyAddress") }}
</q-tooltip>
</q-btn>
</q-item-side>
<q-item-main>
<q-item-tile class="ellipsis" label>{{ address.address }}</q-item-tile>
<q-item-tile v-if="sublabel" sublabel class="non-selectable">{{ sublabel }}</q-item-tile>
</q-item-main>
<q-item-side>
<q-btn flat style="width:25px;" size="md" @click="showQR(address.address, $event)">
<img :src="qrImage" height="20" />
<q-tooltip anchor="bottom right" self="top right" :offset="[0, 5]">
{{ $t("menuItems.showQRCode") }}
</q-tooltip>
</q-btn>
<q-btn flat style="width:25px;" size="md" icon="file_copy" @click="copyAddress(address.address, $event)">
<q-tooltip anchor="bottom right" self="top right" :offset="[0, 5]">
{{ $t("menuItems.copyAddress") }}
</q-tooltip>
</q-btn>
</q-item-side>
</q-item>
<template v-if="shouldShowInfo">
<q-item-separator />
<q-item class="info">
<q-item-main class="flex justify-between">
<div class="column">
<span>{{ $t("strings.lokiBalance") }}</span>
<span class="value">{{address.balance | currency}}</span>
</div>
<div class="column">
<span>{{ $t("strings.lokiUnlockedBalance") }}</span>
<span class="value">{{ address.unlocked_balance | currency }}</span>
</div>
<div class="column">
<span>{{ $t("strings.unspentOutputs") }}</span>
<span class="value">{{ address.num_unspent_outputs | toString }}</span>
</div>
</q-item-main>
</q-item>
<q-item-separator />
<q-item class="info">
<q-item-main class="flex justify-between">
<div class="column">
<span>{{ $t("strings.lokiBalance") }}</span>
<span class="value">{{ address.balance | currency }}</span>
</div>
<div class="column">
<span>{{ $t("strings.lokiUnlockedBalance") }}</span>
<span class="value">{{ address.unlocked_balance | currency }}</span>
</div>
<div class="column">
<span>{{ $t("strings.unspentOutputs") }}</span>
<span class="value">{{ address.num_unspent_outputs | toString }}</span>
</div>
</q-item-main>
</q-item>
</template>
<q-context-menu>
<q-list link separator style="min-width: 150px; max-height: 300px;">
<q-item v-close-overlay
@click.native="details(address)">
<q-item-main :label="$t('menuItems.showDetails')" />
</q-item>
<q-list link separator style="min-width: 150px; max-height: 300px;">
<q-item v-close-overlay @click.native="details(address)">
<q-item-main :label="$t('menuItems.showDetails')" />
</q-item>
<q-item v-close-overlay
@click.native="copyAddress(address.address, $event)">
<q-item-main :label="$t('menuItems.copyAddress')" />
</q-item>
</q-list>
<q-item v-close-overlay @click.native="copyAddress(address.address, $event)">
<q-item-main :label="$t('menuItems.copyAddress')" />
</q-item>
</q-list>
</q-context-menu>
</q-list>
</q-list>
</template>
<script>
import { mapState } from "vuex"
const { clipboard, nativeImage } = require("electron")
export default {
name: "ReceiveItem",
props: {
address: {
required: true
},
sublabel: {
type: String,
required: false,
},
shouldShowInfo: {
type: Boolean,
required: false,
default: true
},
showQR: {
type: Function,
required: true
},
copyAddress: {
type: Function,
required: true
},
details: {
type: Function,
required: true
},
whiteQRIcon: {
type: Boolean,
required: false,
default: false
}
name: "ReceiveItem",
filters: {
toString: function(value) {
if (typeof value !== "number") return "N/A";
return String(value);
},
computed: {
qrImage () {
const image = this.whiteQRIcon ? "qr-code" : "qr-code-grey"
return `statics/${image}.svg`
},
},
filters: {
toString: function (value) {
if (typeof value !== "number") return "N/A";
return String(value);
},
currency: function (value) {
if (typeof value !== "number") return "N/A";
currency: function(value) {
if (typeof value !== "number") return "N/A";
const amount = value / 1e9
return amount.toLocaleString()
}
const amount = value / 1e9;
return amount.toLocaleString();
}
},
props: {
address: {
type: String,
required: true
},
}
sublabel: {
type: String,
required: false,
default: undefined
},
shouldShowInfo: {
type: Boolean,
required: false,
default: true
},
showQR: {
type: Function,
required: true
},
copyAddress: {
type: Function,
required: true
},
details: {
type: Function,
required: true
},
whiteQRIcon: {
type: Boolean,
required: false,
default: false
}
},
computed: {
qrImage() {
const image = this.whiteQRIcon ? "qr-code" : "qr-code-grey";
return `statics/${image}.svg`;
}
}
};
</script>
<style>
</style>
<style></style>

View File

@ -1,211 +1,237 @@
<template>
<q-modal class="serviceNodeDetails" v-model="isVisible" maximized>
<q-modal v-model="isVisible" class="serviceNodeDetails" maximized>
<q-modal-layout>
<q-toolbar slot="header" color="dark" inverted>
<q-btn
flat
round
dense
@click="isVisible = false"
icon="reply"
/>
<q-toolbar-title>
{{ $t("titles.serviceNodeDetails") }}
</q-toolbar-title>
<q-btn
v-if="node.requested_unlock_height === 0"
class="q-mr-sm"
color="primary"
@click="unlock(node.service_node_pubkey, $event)"
:disabled="!is_ready || unlock_status.sending"
:label="$t('buttons.unlock')"
/>
<q-btn v-if="can_open" color="primary" @click="openExplorer" :label="$t('buttons.viewOnExplorer')" />
</q-toolbar>
<q-toolbar slot="header" color="dark" inverted>
<q-btn flat round dense icon="reply" @click="isVisible = false" />
<q-toolbar-title>
{{ $t("titles.serviceNodeDetails") }}
</q-toolbar-title>
<q-btn
v-if="node.requested_unlock_height === 0"
class="q-mr-sm"
color="primary"
:disabled="!is_ready || unlock_status.sending"
:label="$t('buttons.unlock')"
@click="unlock(node.service_node_pubkey, $event)"
/>
<q-btn v-if="can_open" color="primary" :label="$t('buttons.viewOnExplorer')" @click="openExplorer" />
</q-toolbar>
<div class="layout-padding">
<h6 class="q-mt-xs q-mb-none text-weight-light">{{ $t("strings.serviceNodeDetails.serviceNodeKey") }}</h6>
<p class="break-all">{{ node.service_node_pubkey }}</p>
<div class="layout-padding">
<h6 class="q-mt-xs q-mb-none text-weight-light">
{{ $t("strings.serviceNodeDetails.serviceNodeKey") }}
</h6>
<p class="break-all">{{ node.service_node_pubkey }}</p>
<div class="info row justify-between">
<div class="infoBox">
<div class="infoBoxContent">
<div class="text"><span>{{ $t("strings.serviceNodeDetails.stakingRequirement") }}</span></div>
<div class="value"><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></div>
<div class="value"><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></div>
<div class="value"><span>{{ node.registration_height }}</span></div>
</div>
</div>
<div class="infoBox">
<div class="infoBoxContent">
<div class="text"><span>{{ $t("strings.serviceNodeDetails.operatorFee") }}</span></div>
<div class="value"><span>{{ operatorFee }}</span></div>
</div>
</div>
<div class="infoBox" v-if="node.requested_unlock_height > 0">
<div class="infoBoxContent">
<div class="text"><span>{{ $t("strings.serviceNodeDetails.unlockHeight") }}</span></div>
<div class="value"><span>{{ node.requested_unlock_height }}</span></div>
</div>
</div>
<div class="infoBox">
<div class="infoBoxContent">
<div class="text"><span>{{ $t("strings.serviceNodeDetails.lastUptimeProof") }}</span></div>
<div class="value"><span>{{ formatDate(node.last_uptime_proof * 1000) }}</span></div>
</div>
</div>
<div class="infoBox">
<div class="infoBoxContent">
<div class="text"><span>{{ $t("strings.serviceNodeDetails.lastRewardBlockHeight") }}</span></div>
<div class="value"><span>{{ node.last_reward_block_height }}</span></div>
</div>
</div>
<div class="info row justify-between">
<div class="infoBox">
<div class="infoBoxContent">
<div class="text">
<span>{{ $t("strings.serviceNodeDetails.stakingRequirement") }}</span>
</div>
<div class="value">
<span><FormatLoki :amount="node.staking_requirement" raw-value/></span>
</div>
</div>
<q-list no-border :dark="theme=='dark'" class="loki-list">
<q-list-header class="q-px-none">{{ $t("strings.serviceNodeDetails.contributors") }}:</q-list-header>
<q-item class="loki-list-item" v-for="contributor in contributors" :key="contributor.address">
<q-item-main>
<q-item-tile v-if="isMe(contributor)" class="name non-selectable">{{ $t('strings.me') }}</q-item-tile>
<q-item-tile v-else class="name non-selectable">{{ contributor.name }}</q-item-tile>
<q-item-tile class="address ellipsis non-selectable">{{ contributor.address }}</q-item-tile>
<q-item-tile class="non-selectable" sublabel>
<span v-if="isOperator(contributor)">{{ $t('strings.operator') }} </span>
{{ $t('strings.contribution') }}: <FormatLoki :amount="contributor.amount" raw-value />
</q-item-tile>
</q-item-main>
<q-context-menu>
<q-list link separator style="min-width: 150px; max-height: 300px;">
<q-item v-close-overlay
@click.native="copyAddress(contributor.address, $event)">
<q-item-main :label="$t('menuItems.copyAddress')" />
</q-item>
</q-list>
</q-context-menu>
</q-item>
</q-list>
</div>
<div class="infoBox">
<div class="infoBoxContent">
<div class="text">
<span>{{ $t("strings.serviceNodeDetails.totalContributed") }}</span>
</div>
<div class="value">
<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>
</div>
<div class="value">
<span>{{ node.registration_height }}</span>
</div>
</div>
</div>
<div class="infoBox">
<div class="infoBoxContent">
<div class="text">
<span>{{ $t("strings.serviceNodeDetails.operatorFee") }}</span>
</div>
<div class="value">
<span>{{ operatorFee }}</span>
</div>
</div>
</div>
<div v-if="node.requested_unlock_height > 0" class="infoBox">
<div class="infoBoxContent">
<div class="text">
<span>{{ $t("strings.serviceNodeDetails.unlockHeight") }}</span>
</div>
<div class="value">
<span>{{ node.requested_unlock_height }}</span>
</div>
</div>
</div>
<div class="infoBox">
<div class="infoBoxContent">
<div class="text">
<span>{{ $t("strings.serviceNodeDetails.lastUptimeProof") }}</span>
</div>
<div class="value">
<span>{{ formatDate(node.last_uptime_proof * 1000) }}</span>
</div>
</div>
</div>
<div class="infoBox">
<div class="infoBoxContent">
<div class="text">
<span>{{ $t("strings.serviceNodeDetails.lastRewardBlockHeight") }}</span>
</div>
<div class="value">
<span>{{ node.last_reward_block_height }}</span>
</div>
</div>
</div>
</div>
<q-list no-border :dark="theme == 'dark'" class="loki-list">
<q-list-header class="q-px-none">{{ $t("strings.serviceNodeDetails.contributors") }}:</q-list-header>
<q-item v-for="contributor in contributors" :key="contributor.address" class="loki-list-item">
<q-item-main>
<q-item-tile v-if="isMe(contributor)" class="name non-selectable">{{ $t("strings.me") }}</q-item-tile>
<q-item-tile v-else class="name non-selectable">{{ contributor.name }}</q-item-tile>
<q-item-tile class="address ellipsis non-selectable">{{ contributor.address }}</q-item-tile>
<q-item-tile class="non-selectable" sublabel>
<span v-if="isOperator(contributor)">{{ $t("strings.operator") }} </span>
{{ $t("strings.contribution") }}:
<FormatLoki :amount="contributor.amount" raw-value />
</q-item-tile>
</q-item-main>
<q-context-menu>
<q-list link separator style="min-width: 150px; max-height: 300px;">
<q-item v-close-overlay @click.native="copyAddress(contributor.address, $event)">
<q-item-main :label="$t('menuItems.copyAddress')" />
</q-item>
</q-list>
</q-context-menu>
</q-item>
</q-list>
</div>
<q-inner-loading :visible="unlock_status.sending" :dark="theme=='dark'">
<q-spinner color="primary" :size="30" />
</q-inner-loading>
<q-inner-loading :visible="unlock_status.sending" :dark="theme == 'dark'">
<q-spinner color="primary" :size="30" />
</q-inner-loading>
</q-modal-layout>
</q-modal>
</q-modal>
</template>
<script>
const { clipboard } = require("electron")
import { mapState } from "vuex"
import { date } from "quasar"
const { formatDate } = date
import FormatLoki from "components/format_loki"
const { clipboard } = require("electron");
import { mapState } from "vuex";
import { date } from "quasar";
import FormatLoki from "components/format_loki";
export default {
name: "ServiceNodeDetails",
props: {
unlock: {
type: Function,
required: true
},
},
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
unlock_status: state => state.gateway.service_node_status.unlock,
is_ready (state) {
return this.$store.getters["gateway/isReady"]
},
our_address: state => {
const primary = state.gateway.wallet.address_list.primary[0]
return (primary && primary.address) || null
},
can_open (state) {
const { net_type } = state.gateway.app.config.app
return net_type !== "stagenet"
},
contributors (state) {
if (!this.node.contributors) return []
const { address_book } = state.gateway.wallet.address_list
let contributors = []
for (const contributor of this.node.contributors) {
let values = { ...contributor };
const address = address_book.find(a => a.address === contributor.address);
if (address) {
const { name, description} = address
const separator = description === "" ? "" : " - "
values.name = `${name}${separator}${description}`
}
contributors.push(values)
}
return contributors
},
operatorFee () {
const operatorPortion = this.node.portions_for_operator
const percentageFee = operatorPortion / 18446744073709551612 * 100
return `${percentageFee}%`
},
}),
data () {
return {
isVisible: false,
node: {},
}
},
methods: {
openExplorer () {
this.$gateway.send("core", "open_explorer", {type: "service_node", id: this.node.service_node_pubkey})
},
formatDate (timestamp) {
return date.formatDate(timestamp, "YYYY-MM-DD hh:mm a")
},
copyAddress (address, event) {
event.stopPropagation()
for(let i = 0; i < event.path.length; i++) {
if(event.path[i].tagName == "BUTTON") {
event.path[i].blur()
break
}
}
clipboard.writeText(address)
this.$q.notify({
type: "positive",
timeout: 1000,
message: this.$t("notification.positive.addressCopied")
})
},
isMe (contributor) {
return contributor.address === this.our_address
},
isOperator (contributor) {
return this.node.operator_address === contributor.address
},
},
components: {
FormatLoki
name: "ServiceNodeDetails",
components: {
FormatLoki
},
props: {
unlock: {
type: Function,
required: true
}
}
},
data() {
return {
isVisible: false,
node: {}
};
},
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
unlock_status: state => state.gateway.service_node_status.unlock,
is_ready() {
return this.$store.getters["gateway/isReady"];
},
our_address: state => {
const primary = state.gateway.wallet.address_list.primary[0];
return (primary && primary.address) || null;
},
can_open(state) {
const { net_type } = state.gateway.app.config.app;
return net_type !== "stagenet";
},
contributors(state) {
if (!this.node.contributors) return [];
const { address_book } = state.gateway.wallet.address_list;
let contributors = [];
for (const contributor of this.node.contributors) {
let values = { ...contributor };
const address = address_book.find(a => a.address === contributor.address);
if (address) {
const { name, description } = address;
const separator = description === "" ? "" : " - ";
values.name = `${name}${separator}${description}`;
}
contributors.push(values);
}
return contributors;
},
operatorFee() {
const operatorPortion = this.node.portions_for_operator;
const percentageFee = (operatorPortion / 18446744073709551612) * 100;
return `${percentageFee}%`;
}
}),
methods: {
openExplorer() {
this.$gateway.send("core", "open_explorer", {
type: "service_node",
id: this.node.service_node_pubkey
});
},
formatDate(timestamp) {
return date.formatDate(timestamp, "YYYY-MM-DD hh:mm a");
},
copyAddress(address, event) {
event.stopPropagation();
for (let i = 0; i < event.path.length; i++) {
if (event.path[i].tagName == "BUTTON") {
event.path[i].blur();
break;
}
}
clipboard.writeText(address);
this.$q.notify({
type: "positive",
timeout: 1000,
message: this.$t("notification.positive.addressCopied")
});
},
isMe(contributor) {
return contributor.address === this.our_address;
},
isOperator(contributor) {
return this.node.operator_address === contributor.address;
}
}
};
</script>
<style lang="scss">
.serviceNodeDetails {
.name {
font-size: 0.92rem;
}
.name {
font-size: 0.92rem;
}
.infoBox {
margin-left: 30px;
}
.infoBox {
margin-left: 30px;
}
.info {
margin-right: 30px;
}
.info {
margin-right: 30px;
}
}
</style>

View File

@ -1,127 +1,134 @@
<template>
<div class="service-node-registration">
<div class="service-node-registration">
<div class="q-pa-md">
<i18n path="strings.serviceNodeRegistrationDescription" tag="div" class="description q-mb-lg">
<b place="registerCommand">register_service_node</b>
<b place="prepareCommand">prepare_registration</b>
</i18n>
<LokiField :label="$t('fieldLabels.serviceNodeCommand')" :error="$v.registration_string.$error" :disabled="registration_status.sending">
<q-input
v-model.trim="registration_string"
type="textarea"
:dark="theme=='dark'"
@blur="$v.registration_string.$touch"
@paste="onPaste"
placeholder="register_service_node ..."
:disabled="registration_status.sending"
hide-underline
/>
</LokiField>
<q-field class="q-pt-sm">
<q-btn color="primary" @click="register()" :label="$t('buttons.registerServiceNode')" :disabled="registration_status.sending"/>
</q-field>
<i18n path="strings.serviceNodeRegistrationDescription" tag="div" class="description q-mb-lg">
<b place="registerCommand">register_service_node</b>
<b place="prepareCommand">prepare_registration</b>
</i18n>
<LokiField
:label="$t('fieldLabels.serviceNodeCommand')"
:error="$v.registration_string.$error"
:disabled="registration_status.sending"
>
<q-input
v-model.trim="registration_string"
type="textarea"
:dark="theme == 'dark'"
placeholder="register_service_node ..."
:disabled="registration_status.sending"
hide-underline
@blur="$v.registration_string.$touch"
@paste="onPaste"
/>
</LokiField>
<q-field class="q-pt-sm">
<q-btn
color="primary"
:label="$t('buttons.registerServiceNode')"
:disabled="registration_status.sending"
@click="register()"
/>
</q-field>
</div>
<q-inner-loading :visible="registration_status.sending" :dark="theme=='dark'">
<q-spinner color="primary" :size="30" />
<q-inner-loading :visible="registration_status.sending" :dark="theme == 'dark'">
<q-spinner color="primary" :size="30" />
</q-inner-loading>
</div>
</div>
</template>
<script>
const objectAssignDeep = require("object-assign-deep");
import { mapState } from "vuex"
import { required } from "vuelidate/lib/validators"
import LokiField from "components/loki_field"
import WalletPassword from "src/mixins/wallet_password"
import { mapState } from "vuex";
import { required } from "vuelidate/lib/validators";
import LokiField from "components/loki_field";
import WalletPassword from "src/mixins/wallet_password";
export default {
name: "ServiceNodeRegistration",
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
registration_status: state => state.gateway.service_node_status.registration,
}),
data () {
return {
registration_string: "",
}
},
validations: {
registration_string: { required }
},
watch: {
registration_status: {
handler(val, old){
if(val.code == old.code) return
switch(this.registration_status.code) {
case 0:
this.$q.notify({
type: "positive",
timeout: 1000,
message: this.registration_status.message
})
this.$v.$reset();
this.registration_string = ""
break;
case -1:
this.$q.notify({
type: "negative",
timeout: 3000,
message: this.registration_status.message
})
break;
}
},
deep: true
},
},
methods: {
register() {
this.$v.registration_string.$touch()
if (this.$v.registration_string.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.invalidServiceNodeCommand")
})
return
}
this.showPasswordConfirmation({
title: this.$t("dialog.registerServiceNode.title"),
noPasswordMessage: this.$t("dialog.registerServiceNode.message"),
ok: {
label: this.$t("dialog.registerServiceNode.ok")
},
}).then(password => {
this.$store.commit("gateway/set_snode_status", {
registration: {
code: 1,
message: "Registering...",
sending: true
}
})
this.$gateway.send("wallet", "register_service_node", {
password,
string: this.registration_string.trim()
})
}).catch(() => {
})
},
onPaste (event) {
this.$nextTick(() => {
this.registration_string = this.registration_string.trim()
name: "ServiceNodeRegistration",
components: {
LokiField
},
mixins: [WalletPassword],
data() {
return {
registration_string: ""
};
},
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
registration_status: state => state.gateway.service_node_status.registration
}),
validations: {
registration_string: { required }
},
watch: {
registration_status: {
handler(val, old) {
if (val.code == old.code) return;
switch (this.registration_status.code) {
case 0:
this.$q.notify({
type: "positive",
timeout: 1000,
message: this.registration_status.message
});
this.$v.$reset();
this.registration_string = "";
break;
case -1:
this.$q.notify({
type: "negative",
timeout: 3000,
message: this.registration_status.message
});
break;
}
},
mixins: [WalletPassword],
components: {
LokiField
},
deep: true
}
}
},
methods: {
register() {
this.$v.registration_string.$touch();
if (this.$v.registration_string.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.invalidServiceNodeCommand")
});
return;
}
this.showPasswordConfirmation({
title: this.$t("dialog.registerServiceNode.title"),
noPasswordMessage: this.$t("dialog.registerServiceNode.message"),
ok: {
label: this.$t("dialog.registerServiceNode.ok")
}
})
.then(password => {
this.$store.commit("gateway/set_snode_status", {
registration: {
code: 1,
message: "Registering...",
sending: true
}
});
this.$gateway.send("wallet", "register_service_node", {
password,
string: this.registration_string.trim()
});
})
.catch(() => {});
},
onPaste() {
this.$nextTick(() => {
this.registration_string = this.registration_string.trim();
});
}
}
};
</script>
<style lang="scss">
</style>
<style lang="scss"></style>

View File

@ -1,275 +1,282 @@
<template>
<div class="service-node-staking">
<div class="service-node-staking">
<div class="q-px-md q-pt-md">
<LokiField :label="$t('fieldLabels.serviceNodeKey')" :error="$v.service_node.key.$error">
<q-input v-model.trim="service_node.key"
:dark="theme=='dark'"
@blur="$v.service_node.key.$touch"
:placeholder="$t('placeholders.hexCharacters', { count: 64 })"
hide-underline
/>
</LokiField>
<LokiField :label="$t('fieldLabels.serviceNodeKey')" :error="$v.service_node.key.$error">
<q-input
v-model.trim="service_node.key"
:dark="theme == 'dark'"
:placeholder="$t('placeholders.hexCharacters', { count: 64 })"
hide-underline
@blur="$v.service_node.key.$touch"
/>
</LokiField>
<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'"
type="number"
min="0"
:max="unlocked_balance / 1e9"
placeholder="0"
@blur="$v.service_node.amount.$touch"
hide-underline
/>
<q-btn color="secondary" @click="service_node.amount = unlocked_balance / 1e9" :text-color="theme=='dark'?'white':'dark'">
{{ $t("buttons.all") }}
</q-btn>
</LokiField>
<q-field class="buttons q-pt-sm">
<q-btn
:disable="!is_able_to_send"
color="primary" @click="stake()" :label="$t('buttons.stake')" />
<q-btn
:disable="!is_able_to_send"
color="secondary" @click="sweepAllWarning()" :label="$t('buttons.sweepAll')" />
</q-field>
<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'"
type="number"
min="0"
:max="unlocked_balance / 1e9"
placeholder="0"
hide-underline
@blur="$v.service_node.amount.$touch"
/>
<q-btn
color="secondary"
:text-color="theme == 'dark' ? 'white' : 'dark'"
@click="service_node.amount = unlocked_balance / 1e9"
>
{{ $t("buttons.all") }}
</q-btn>
</LokiField>
<q-field class="buttons q-pt-sm">
<q-btn :disable="!is_able_to_send" color="primary" :label="$t('buttons.stake')" @click="stake()" />
<q-btn
:disable="!is_able_to_send"
color="secondary"
:label="$t('buttons.sweepAll')"
@click="sweepAllWarning()"
/>
</q-field>
</div>
<ServiceNodeUnlock />
<q-inner-loading :visible="stake_status.sending || tx_status.sending" :dark="theme=='dark'">
<q-spinner color="primary" :size="30" />
<q-inner-loading :visible="stake_status.sending || tx_status.sending" :dark="theme == 'dark'">
<q-spinner color="primary" :size="30" />
</q-inner-loading>
</div>
</div>
</template>
<script>
const { clipboard } = require("electron")
const objectAssignDeep = require("object-assign-deep");
import { mapState } from "vuex"
import { required, decimal } from "vuelidate/lib/validators"
import { i18n } from "plugins/i18n"
import { payment_id, service_node_key, greater_than_zero, address } from "src/validators/common"
import LokiField from "components/loki_field"
import WalletPassword from "src/mixins/wallet_password"
import ServiceNodeUnlock from "components/service_node_unlock"
import { mapState } from "vuex";
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 ServiceNodeUnlock from "components/service_node_unlock";
export default {
name: "ServiceNodeStaking",
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
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,
award_address: state => state.gateway.wallet.info.address,
is_ready (state) {
return this.$store.getters["gateway/isReady"]
},
is_able_to_send (state) {
return this.$store.getters["gateway/isAbleToSend"]
},
address_placeholder (state) {
const wallet = state.gateway.wallet.info;
const prefix = (wallet && wallet.address && wallet.address[0]) || "L";
return `${prefix}..`;
},
}),
data () {
return {
service_node: {
key: "",
amount: 0,
},
}
name: "ServiceNodeStaking",
components: {
LokiField,
ServiceNodeUnlock
},
mixins: [WalletPassword],
data() {
return {
service_node: {
key: "",
amount: 0
}
};
},
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
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,
award_address: state => state.gateway.wallet.info.address,
is_ready() {
return this.$store.getters["gateway/isReady"];
},
validations: {
service_node: {
key: { required, service_node_key },
amount: {
required,
decimal,
greater_than_zero,
},
}
is_able_to_send() {
return this.$store.getters["gateway/isAbleToSend"];
},
watch: {
stake_status: {
handler(val, old){
if(val.code == old.code) return
switch(this.stake_status.code) {
case 0:
this.$q.notify({
type: "positive",
timeout: 1000,
message: this.stake_status.message
})
this.$v.$reset();
this.service_node = {
key: "",
amount: 0,
}
break;
case -1:
this.$q.notify({
type: "negative",
timeout: 3000,
message: this.stake_status.message
})
break;
}
},
deep: true
},
tx_status: {
handler(val, old){
if(val.code == old.code) return
switch(this.tx_status.code) {
case 0:
this.$q.notify({
type: "positive",
timeout: 1000,
message: this.tx_status.message
})
break;
case -1:
this.$q.notify({
type: "negative",
timeout: 3000,
message: this.tx_status.message
})
break;
}
},
deep: true
},
},
methods: {
sweepAllWarning: function () {
this.$q.dialog({
title: this.$t("dialog.sweepAllWarning.title"),
message: this.$t("dialog.sweepAllWarning.message"),
ok: {
label: this.$t("dialog.sweepAllWarning.ok")
},
cancel: {
flat: true,
label: this.$t("dialog.buttons.cancel"),
color: this.theme === "dark" ? "white" : "dark"
}
}).then(() => {
this.sweepAll()
}).catch(() => {})
},
sweepAll: function () {
const { unlocked_balance } = this.info;
const tx = {
amount: unlocked_balance / 1e9,
address: this.award_address,
priority: 0
};
this.showPasswordConfirmation({
title: this.$t("dialog.sweepAll.title"),
noPasswordMessage: this.$t("dialog.sweepAll.message"),
ok: {
label: this.$t("dialog.sweepAll.ok")
},
}).then(password => {
this.$store.commit("gateway/set_tx_status", {
code: 1,
message: "Sweeping all",
sending: true
})
const newTx = objectAssignDeep.noMutate(tx, {password})
this.$gateway.send("wallet", "transfer", newTx)
}).catch(() => {
})
},
stake: function () {
this.$v.service_node.$touch()
if (this.$v.service_node.key.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.invalidServiceNodeKey")
})
return
}
if(this.service_node.amount < 0) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.negativeAmount")
})
return
} else if(this.service_node.amount == 0) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.zeroAmount")
})
return
} else if(this.service_node.amount > this.unlocked_balance / 1e9) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.notEnoughBalance")
})
return
} else if (this.$v.service_node.amount.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.invalidAmount")
})
return
}
this.showPasswordConfirmation({
title: this.$t("dialog.stake.title"),
noPasswordMessage: this.$t("dialog.stake.message"),
ok: {
label: this.$t("dialog.stake.ok")
},
}).then(password => {
this.$store.commit("gateway/set_snode_status", {
stake: {
code: 1,
message: "Staking...",
sending: true
}
})
const service_node = objectAssignDeep.noMutate(this.service_node, {
password,
destination: this.award_address
})
this.$gateway.send("wallet", "stake", service_node)
}).catch(() => {
})
}
},
mixins: [WalletPassword],
components: {
LokiField,
ServiceNodeUnlock
address_placeholder(state) {
const wallet = state.gateway.wallet.info;
const prefix = (wallet && wallet.address && wallet.address[0]) || "L";
return `${prefix}..`;
}
}
}),
validations: {
service_node: {
key: { required, service_node_key },
amount: {
required,
decimal,
greater_than_zero
}
}
},
watch: {
stake_status: {
handler(val, old) {
if (val.code == old.code) return;
switch (this.stake_status.code) {
case 0:
this.$q.notify({
type: "positive",
timeout: 1000,
message: this.stake_status.message
});
this.$v.$reset();
this.service_node = {
key: "",
amount: 0
};
break;
case -1:
this.$q.notify({
type: "negative",
timeout: 3000,
message: this.stake_status.message
});
break;
}
},
deep: true
},
tx_status: {
handler(val, old) {
if (val.code == old.code) return;
switch (this.tx_status.code) {
case 0:
this.$q.notify({
type: "positive",
timeout: 1000,
message: this.tx_status.message
});
break;
case -1:
this.$q.notify({
type: "negative",
timeout: 3000,
message: this.tx_status.message
});
break;
}
},
deep: true
}
},
methods: {
sweepAllWarning: function() {
this.$q
.dialog({
title: this.$t("dialog.sweepAllWarning.title"),
message: this.$t("dialog.sweepAllWarning.message"),
ok: {
label: this.$t("dialog.sweepAllWarning.ok")
},
cancel: {
flat: true,
label: this.$t("dialog.buttons.cancel"),
color: this.theme === "dark" ? "white" : "dark"
}
})
.then(() => {
this.sweepAll();
})
.catch(() => {});
},
sweepAll: function() {
const { unlocked_balance } = this.info;
const tx = {
amount: unlocked_balance / 1e9,
address: this.award_address,
priority: 0
};
this.showPasswordConfirmation({
title: this.$t("dialog.sweepAll.title"),
noPasswordMessage: this.$t("dialog.sweepAll.message"),
ok: {
label: this.$t("dialog.sweepAll.ok")
}
})
.then(password => {
this.$store.commit("gateway/set_tx_status", {
code: 1,
message: "Sweeping all",
sending: true
});
const newTx = objectAssignDeep.noMutate(tx, { password });
this.$gateway.send("wallet", "transfer", newTx);
})
.catch(() => {});
},
stake: function() {
this.$v.service_node.$touch();
if (this.$v.service_node.key.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.invalidServiceNodeKey")
});
return;
}
if (this.service_node.amount < 0) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.negativeAmount")
});
return;
} else if (this.service_node.amount == 0) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.zeroAmount")
});
return;
} else if (this.service_node.amount > this.unlocked_balance / 1e9) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.notEnoughBalance")
});
return;
} else if (this.$v.service_node.amount.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.invalidAmount")
});
return;
}
this.showPasswordConfirmation({
title: this.$t("dialog.stake.title"),
noPasswordMessage: this.$t("dialog.stake.message"),
ok: {
label: this.$t("dialog.stake.ok")
}
})
.then(password => {
this.$store.commit("gateway/set_snode_status", {
stake: {
code: 1,
message: "Staking...",
sending: true
}
});
const service_node = objectAssignDeep.noMutate(this.service_node, {
password,
destination: this.award_address
});
this.$gateway.send("wallet", "stake", service_node);
})
.catch(() => {});
}
}
};
</script>
<style lang="scss">
.service-node-staking {
.buttons {
.q-btn:not(:first-child) {
margin-left: 8px;
}
.buttons {
.q-btn:not(:first-child) {
margin-left: 8px;
}
}
}
</style>

View File

@ -1,234 +1,253 @@
<template>
<div class="service-node-unlock" v-if="service_nodes.length > 0">
<div v-if="service_nodes.length > 0" class="service-node-unlock">
<div class="q-pa-md">
<div class="q-pb-sm header">{{ $t('titles.currentlyStakedNodes') }}</div>
<q-list class="service-node-list" no-border>
<q-item v-for="node in service_nodes" :key="node.service_node_pubkey" @click.native="details(node)">
<q-item-main>
<q-item-tile class="ellipsis" label>{{ node.service_node_pubkey }}</q-item-tile>
<q-item-tile sublabel class="non-selectable">{{ getRole(node) }} {{ getFee(node) }} {{ $t('strings.contribution') }}: <FormatLoki :amount="node.ourContributionAmount" /></q-item-tile>
</q-item-main>
<q-item-side>
<q-btn
v-if="node.requested_unlock_height === 0"
color="primary"
size="md"
:label="$t('buttons.unlock')"
:disabled="!is_ready || unlock_status.sending"
@click.native="unlockWarning(node.service_node_pubkey, $event)"
/>
<q-item-tile label v-if="node.requested_unlock_height > 0">
{{ $t('strings.unlockingAtHeight', { number: node.requested_unlock_height }) }}
</q-item-tile>
</q-item-side>
<q-context-menu>
<q-list link separator style="min-width: 150px; max-height: 300px;">
<q-item v-close-overlay @click.native="copyKey(node.service_node_pubkey, $event)">
<q-item-main :label="$t('menuItems.copyServiceNodeKey')" />
</q-item>
<q-item v-close-overlay @click.native="openExplorer(node.service_node_pubkey)">
<q-item-main :label="$t('menuItems.viewOnExplorer')" />
</q-item>
</q-list>
</q-context-menu>
</q-item>
</q-list>
<div class="q-pb-sm header">
{{ $t("titles.currentlyStakedNodes") }}
</div>
<q-list class="service-node-list" no-border>
<q-item v-for="node in service_nodes" :key="node.service_node_pubkey" @click.native="details(node)">
<q-item-main>
<q-item-tile class="ellipsis" label>{{ node.service_node_pubkey }}</q-item-tile>
<q-item-tile sublabel class="non-selectable"
>{{ getRole(node) }} {{ getFee(node) }} {{ $t("strings.contribution") }}:
<FormatLoki :amount="node.ourContributionAmount"
/></q-item-tile>
</q-item-main>
<q-item-side>
<q-btn
v-if="node.requested_unlock_height === 0"
color="primary"
size="md"
:label="$t('buttons.unlock')"
:disabled="!is_ready || unlock_status.sending"
@click.native="unlockWarning(node.service_node_pubkey, $event)"
/>
<q-item-tile v-if="node.requested_unlock_height > 0" label>
{{
$t("strings.unlockingAtHeight", {
number: node.requested_unlock_height
})
}}
</q-item-tile>
</q-item-side>
<q-context-menu>
<q-list link separator style="min-width: 150px; max-height: 300px;">
<q-item v-close-overlay @click.native="copyKey(node.service_node_pubkey, $event)">
<q-item-main :label="$t('menuItems.copyServiceNodeKey')" />
</q-item>
<q-item v-close-overlay @click.native="openExplorer(node.service_node_pubkey)">
<q-item-main :label="$t('menuItems.viewOnExplorer')" />
</q-item>
</q-list>
</q-context-menu>
</q-item>
</q-list>
</div>
<ServiceNodeDetails ref="serviceNodeDetails" :unlock="unlockWarning" />
<q-inner-loading :visible="unlock_status.sending" :dark="theme=='dark'">
<q-spinner color="primary" :size="30" />
<q-inner-loading :visible="unlock_status.sending" :dark="theme == 'dark'">
<q-spinner color="primary" :size="30" />
</q-inner-loading>
</div>
</div>
</template>
<script>
const objectAssignDeep = require("object-assign-deep");
import { clipboard } from "electron"
import { mapState } from "vuex"
import { required } from "vuelidate/lib/validators"
import { service_node_key } from "src/validators/common"
import LokiField from "components/loki_field"
import WalletPassword from "src/mixins/wallet_password"
import FormatLoki from "components/format_loki"
import ServiceNodeDetails from "components/service_node_details"
import { clipboard } from "electron";
import { mapState } from "vuex";
import { required } from "vuelidate/lib/validators";
import { service_node_key } from "src/validators/common";
import WalletPassword from "src/mixins/wallet_password";
import FormatLoki from "components/format_loki";
import ServiceNodeDetails from "components/service_node_details";
export default {
name: "ServiceNodeUnlock",
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
unlock_status: state => state.gateway.service_node_status.unlock,
our_address: state => {
const primary = state.gateway.wallet.address_list.primary[0]
return (primary && primary.address) || null
},
is_ready (state) {
return this.$store.getters["gateway/isReady"]
},
service_nodes (state) {
const nodes = state.gateway.daemon.service_nodes
const getContribution = node => node.contributors.find(c => c.address === this.our_address)
// Only show nodes that we contributed to
return nodes.filter(getContribution).map(n => {
const ourContribution = getContribution(n)
return {
...n,
ourContributionAmount: ourContribution.amount
}
})
}
}),
validations: {
node_key: { required, service_node_key }
name: "ServiceNodeUnlock",
components: {
FormatLoki,
ServiceNodeDetails
},
mixins: [WalletPassword],
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
unlock_status: state => state.gateway.service_node_status.unlock,
our_address: state => {
const primary = state.gateway.wallet.address_list.primary[0];
return (primary && primary.address) || null;
},
watch: {
unlock_status: {
handler(val, old){
if(val.code == old.code) return
switch(this.unlock_status.code) {
case 0:
this.key = null
this.password = null
this.$q.notify({
type: "positive",
timeout: 1000,
message: this.unlock_status.message
})
this.$v.$reset();
break;
case 1:
// Tell the user to confirm
this.$q.dialog({
title: this.$t("dialog.unlockServiceNode.confirmTitle"),
message: this.unlock_status.message,
ok: {
label: this.$t("dialog.unlockServiceNode.ok")
},
cancel: {
flat: true,
label: this.$t("dialog.buttons.cancel"),
color: this.theme=="dark"?"white":"dark"
}
}).then(() => {
this.gatewayUnlock(this.password, this.key, true);
}).catch(() => {
})
break;
case -1:
this.key = null
this.password = null
this.$q.notify({
type: "negative",
timeout: 3000,
message: this.unlock_status.message
})
break;
default: break;
}
},
deep: true
},
is_ready() {
return this.$store.getters["gateway/isReady"];
},
methods: {
details (node) {
this.$refs.serviceNodeDetails.isVisible = true
this.$refs.serviceNodeDetails.node = node
},
unlockWarning (key, event) {
event.stopPropagation()
this.$q.dialog({
title: this.$t("dialog.unlockServiceNodeWarning.title"),
message: this.$t("dialog.unlockServiceNodeWarning.message"),
service_nodes(state) {
const nodes = state.gateway.daemon.service_nodes;
const getContribution = node => node.contributors.find(c => c.address === this.our_address);
// Only show nodes that we contributed to
return nodes.filter(getContribution).map(n => {
const ourContribution = getContribution(n);
return {
...n,
ourContributionAmount: ourContribution.amount
};
});
}
}),
validations: {
node_key: { required, service_node_key }
},
watch: {
unlock_status: {
handler(val, old) {
if (val.code == old.code) return;
switch (this.unlock_status.code) {
case 0:
this.key = null;
this.password = null;
this.$q.notify({
type: "positive",
timeout: 1000,
message: this.unlock_status.message
});
this.$v.$reset();
break;
case 1:
// Tell the user to confirm
this.$q
.dialog({
title: this.$t("dialog.unlockServiceNode.confirmTitle"),
message: this.unlock_status.message,
ok: {
label: this.$t("dialog.unlockServiceNodeWarning.ok")
label: this.$t("dialog.unlockServiceNode.ok")
},
cancel: {
flat: true,
label: this.$t("dialog.buttons.cancel"),
color: this.theme === "dark" ? "white" : "dark"
flat: true,
label: this.$t("dialog.buttons.cancel"),
color: this.theme == "dark" ? "white" : "dark"
}
}).then(() => {
this.unlock(key)
}).catch(() => {})
},
unlock (key) {
// We store this as it could change between the 2 step process
this.key = key
})
.then(() => {
this.gatewayUnlock(this.password, this.key, true);
})
.catch(() => {});
break;
case -1:
this.key = null;
this.password = null;
this.showPasswordConfirmation({
title: this.$t("dialog.unlockServiceNode.title"),
noPasswordMessage: this.$t("dialog.unlockServiceNode.message"),
ok: {
label: this.$t("dialog.unlockServiceNode.ok")
},
}).then(password => {
this.password = password
this.gatewayUnlock(this.password, this.key, false);
}).catch(() => {
})
},
gatewayUnlock (password, key, confirmed = false) {
this.$store.commit("gateway/set_snode_status", {
unlock: {
code: 2, // Code 1 is reserved for can_unlock
message: "Unlocking...",
sending: true
}
})
this.$gateway.send("wallet", "unlock_stake", {
password,
service_node_key: key,
confirmed
})
},
copyKey (key, event) {
event.stopPropagation()
for(let i = 0; i < event.path.length; i++) {
if(event.path[i].tagName == "BUTTON") {
event.path[i].blur()
break
}
}
clipboard.writeText(key)
this.$q.notify({
type: "positive",
timeout: 1000,
message: this.$t("notification.positive.copied", { item: "Service node key" })
})
},
openExplorer (key) {
this.$gateway.send("core", "open_explorer", {type: "service_node", id: key})
},
getRole (node) {
const key = node.operator_address === this.our_address ? 'strings.operator' : 'strings.contributor'
return this.$t(key);
},
getFee (node) {
const operatorPortion = node.portions_for_operator
const percentageFee = operatorPortion / 18446744073709551612 * 100
return `${percentageFee}% ${this.$t('strings.transactions.fee')}`
},
},
mixins: [WalletPassword],
components: {
LokiField,
FormatLoki,
ServiceNodeDetails
type: "negative",
timeout: 3000,
message: this.unlock_status.message
});
break;
default:
break;
}
},
deep: true
}
}
},
methods: {
details(node) {
this.$refs.serviceNodeDetails.isVisible = true;
this.$refs.serviceNodeDetails.node = node;
},
unlockWarning(key, event) {
event.stopPropagation();
this.$q
.dialog({
title: this.$t("dialog.unlockServiceNodeWarning.title"),
message: this.$t("dialog.unlockServiceNodeWarning.message"),
ok: {
label: this.$t("dialog.unlockServiceNodeWarning.ok")
},
cancel: {
flat: true,
label: this.$t("dialog.buttons.cancel"),
color: this.theme === "dark" ? "white" : "dark"
}
})
.then(() => {
this.unlock(key);
})
.catch(() => {});
},
unlock(key) {
// We store this as it could change between the 2 step process
this.key = key;
this.showPasswordConfirmation({
title: this.$t("dialog.unlockServiceNode.title"),
noPasswordMessage: this.$t("dialog.unlockServiceNode.message"),
ok: {
label: this.$t("dialog.unlockServiceNode.ok")
}
})
.then(password => {
this.password = password;
this.gatewayUnlock(this.password, this.key, false);
})
.catch(() => {});
},
gatewayUnlock(password, key, confirmed = false) {
this.$store.commit("gateway/set_snode_status", {
unlock: {
code: 2, // Code 1 is reserved for can_unlock
message: "Unlocking...",
sending: true
}
});
this.$gateway.send("wallet", "unlock_stake", {
password,
service_node_key: key,
confirmed
});
},
copyKey(key, event) {
event.stopPropagation();
for (let i = 0; i < event.path.length; i++) {
if (event.path[i].tagName == "BUTTON") {
event.path[i].blur();
break;
}
}
clipboard.writeText(key);
this.$q.notify({
type: "positive",
timeout: 1000,
message: this.$t("notification.positive.copied", {
item: "Service node key"
})
});
},
openExplorer(key) {
this.$gateway.send("core", "open_explorer", {
type: "service_node",
id: key
});
},
getRole(node) {
const key = node.operator_address === this.our_address ? "strings.operator" : "strings.contributor";
return this.$t(key);
},
getFee(node) {
const operatorPortion = node.portions_for_operator;
const percentageFee = (operatorPortion / 18446744073709551612) * 100;
return `${percentageFee}% ${this.$t("strings.transactions.fee")}`;
}
}
};
</script>
<style lang="scss">
.service-node-unlock {
user-select: none;
.header {
font-weight: 450;
}
.q-item-sublabel, .q-list-header {
font-size: 14px;
}
user-select: none;
.header {
font-weight: 450;
}
.q-item-sublabel,
.q-list-header {
font-size: 14px;
}
}
</style>

View File

@ -1,156 +1,163 @@
<template>
<q-modal v-model="isVisible" maximized class="settings-modal">
<q-modal v-model="isVisible" maximized class="settings-modal">
<q-modal-layout>
<q-toolbar slot="header" color="dark" inverted>
<q-btn flat round dense @click="isVisible = false" icon="reply" />
<q-toolbar-title shrink>
{{ $t("titles.settings.title") }}
</q-toolbar-title>
<q-toolbar slot="header" color="dark" inverted>
<q-btn flat round dense icon="reply" @click="isVisible = false" />
<q-toolbar-title shrink>
{{ $t("titles.settings.title") }}
</q-toolbar-title>
<div class="row col justify-center q-pr-xl">
<q-btn-toggle
v-model="page"
toggle-color="primary"
color="tertiary"
size="md"
:options="tabs"
/>
</div>
<q-btn color="primary" @click="save" :label="$t('buttons.save')" />
</q-toolbar>
<div v-if="page=='general'">
<div class="q-pa-lg">
<SettingsGeneral ref="settingsGeneral"></SettingsGeneral>
</div>
<div class="row col justify-center q-pr-xl">
<q-btn-toggle v-model="page" toggle-color="primary" color="tertiary" size="md" :options="tabs" />
</div>
<div v-if="page=='peers'">
<q-list :dark="theme=='dark'" no-border>
<q-list-header>{{ $t("strings.peerList") }}</q-list-header>
<q-item link v-for="(entry, index) in daemon.connections" @click.native="showPeerDetails(entry)">
<q-item-main>
<q-item-tile label>{{ entry.address }}</q-item-tile>
<q-item-tile sublabel>{{ $t("strings.blockHeight") }}: {{ entry.height }}</q-item-tile>
</q-item-main>
</q-item>
<template v-if="daemon.bans.length">
<q-list-header>{{ $t("strings.bannedPeers.title") }}</q-list-header>
<q-item v-for="(entry, index) in daemon.bans">
<q-item-main>
<q-item-tile label>{{ entry.host }}</q-item-tile>
<q-item-tile sublabel>{{ $t("strings.bannedPeers.bannedUntil", { time: new Date(Date.now() + entry.seconds * 1000).toLocaleString() }) }}</q-item-tile>
</q-item-main>
</q-item>
</template>
</q-list>
<q-btn color="primary" :label="$t('buttons.save')" @click="save" />
</q-toolbar>
<div v-if="page == 'general'">
<div class="q-pa-lg">
<SettingsGeneral ref="settingsGeneral"></SettingsGeneral>
</div>
</div>
<div v-if="page === 'language'">
<language-select />
</div>
<div v-if="page == 'peers'">
<q-list :dark="theme == 'dark'" no-border>
<q-list-header>{{ $t("strings.peerList") }}</q-list-header>
<q-item v-for="entry in daemon.connections" :key="entry.address" link @click.native="showPeerDetails(entry)">
<q-item-main>
<q-item-tile label>{{ entry.address }}</q-item-tile>
<q-item-tile sublabel>{{ $t("strings.blockHeight") }}: {{ entry.height }}</q-item-tile>
</q-item-main>
</q-item>
<template v-if="daemon.bans.length">
<q-list-header>{{ $t("strings.bannedPeers.title") }}</q-list-header>
<q-item v-for="entry in daemon.bans" :key="entry.host">
<q-item-main>
<q-item-tile label>{{ entry.host }}</q-item-tile>
<q-item-tile sublabel>{{
$t("strings.bannedPeers.bannedUntil", {
time: new Date(Date.now() + entry.seconds * 1000).toLocaleString()
})
}}</q-item-tile>
</q-item-main>
</q-item>
</template>
</q-list>
</div>
<div v-if="page === 'language'">
<LanguageSelect />
</div>
</q-modal-layout>
</q-modal>
</q-modal>
</template>
<script>
import { mapState } from "vuex"
import SettingsGeneral from "components/settings_general"
import LanguageSelect from "components/language_select"
import { mapState } from "vuex";
import SettingsGeneral from "components/settings_general";
import LanguageSelect from "components/language_select";
export default {
name: "SettingsModal",
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
daemon: state => state.gateway.daemon,
pending_config: state => state.gateway.app.pending_config,
config: state => state.gateway.app.config,
tabs: function(state) {
const { app, daemons } = state.gateway.app.config;
let tabs = [
{ label: this.$t("titles.settings.tabs.general"), value: 'general', icon: 'settings' },
{ label: this.$t("titles.settings.tabs.language"), value: 'language', icon: 'language' },
]
if(daemons[app.net_type].type != 'remote') {
tabs.push({ label: this.$t("titles.settings.tabs.peers"), value: 'peers', icon: 'cloud_queue' })
}
return tabs
}
}),
data () {
return {
page: "general",
isVisible: false
}
},
watch: {
isVisible: function () {
if(this.isVisible == false) {
this.$store.dispatch("gateway/resetPendingConfig")
}
}
},
methods: {
save() {
this.$gateway.send("core", "save_config", this.pending_config);
this.isVisible = false
name: "SettingsModal",
components: {
LanguageSelect,
SettingsGeneral
},
data() {
return {
page: "general",
isVisible: false
};
},
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
daemon: state => state.gateway.daemon,
pending_config: state => state.gateway.app.pending_config,
config: state => state.gateway.app.config,
tabs: function(state) {
const { app, daemons } = state.gateway.app.config;
let tabs = [
{
label: this.$t("titles.settings.tabs.general"),
value: "general",
icon: "settings"
},
showPeerDetails (entry) {
this.$q.dialog({
title: this.$t("dialog.banPeer.peerDetailsTitle"),
message: JSON.stringify(entry, null, 2),
ok: {
label: this.$t("dialog.banPeer.ok"),
color: "negative",
},
cancel: {
flat: true,
label: "Close",
color: this.theme=="dark"?"white":"dark"
}
}).then(() => {
this.$q.dialog({
title: this.$t("dialog.banPeer.title"),
message: this.$t("dialog.banPeer.message"),
prompt: {
model: "",
type: "number"
},
ok: {
label: this.$t("dialog.banPeer.ok"),
color: "negative"
},
cancel: {
flat: true,
label: this.$t("dialog.buttons.cancel"),
color: this.theme=="dark"?"white":"dark"
}
}).then(seconds => {
this.$gateway.send("daemon", "ban_peer", {host: entry.host, seconds})
}).catch(() => {
})
}).catch(() => {
})
{
label: this.$t("titles.settings.tabs.language"),
value: "language",
icon: "language"
}
},
components: {
LanguageSelect,
SettingsGeneral
];
if (daemons[app.net_type].type != "remote") {
tabs.push({
label: this.$t("titles.settings.tabs.peers"),
value: "peers",
icon: "cloud_queue"
});
}
return tabs;
}
}
}),
watch: {
isVisible: function() {
if (this.isVisible == false) {
this.$store.dispatch("gateway/resetPendingConfig");
}
}
},
methods: {
save() {
this.$gateway.send("core", "save_config", this.pending_config);
this.isVisible = false;
},
showPeerDetails(entry) {
this.$q
.dialog({
title: this.$t("dialog.banPeer.peerDetailsTitle"),
message: JSON.stringify(entry, null, 2),
ok: {
label: this.$t("dialog.banPeer.ok"),
color: "negative"
},
cancel: {
flat: true,
label: "Close",
color: this.theme == "dark" ? "white" : "dark"
}
})
.then(() => {
this.$q
.dialog({
title: this.$t("dialog.banPeer.title"),
message: this.$t("dialog.banPeer.message"),
prompt: {
model: "",
type: "number"
},
ok: {
label: this.$t("dialog.banPeer.ok"),
color: "negative"
},
cancel: {
flat: true,
label: this.$t("dialog.buttons.cancel"),
color: this.theme == "dark" ? "white" : "dark"
}
})
.then(seconds => {
this.$gateway.send("daemon", "ban_peer", {
host: entry.host,
seconds
});
})
.catch(() => {});
})
.catch(() => {});
}
}
};
</script>
<style lang="scss">
</style>
<style lang="scss"></style>

View File

@ -1,266 +1,388 @@
<template>
<div class="settings-general">
<div class="settings-general">
<div class="row justify-between q-mb-md">
<div><q-radio v-model="config_daemon.type" val="remote" :label="$t('strings.daemon.remote.title')" /></div>
<div><q-radio v-model="config_daemon.type" val="local_remote" :label="$t('strings.daemon.localRemote.title')" /></div>
<div><q-radio v-model="config_daemon.type" val="local" :label="$t('strings.daemon.local.title')" /></div>
<div>
<q-radio v-model="config_daemon.type" val="remote" :label="$t('strings.daemon.remote.title')" />
</div>
<div>
<q-radio v-model="config_daemon.type" val="local_remote" :label="$t('strings.daemon.localRemote.title')" />
</div>
<div>
<q-radio v-model="config_daemon.type" val="local" :label="$t('strings.daemon.local.title')" />
</div>
</div>
<p v-if="config_daemon.type == 'local_remote'">
{{ $t("strings.daemon.localRemote.description") }}
{{ $t("strings.daemon.localRemote.description") }}
</p>
<p v-if="config_daemon.type == 'local'">
{{ $t("strings.daemon.local.description") }}
{{ $t("strings.daemon.local.description") }}
</p>
<p v-if="is_remote">
{{ $t("strings.daemon.remote.description") }}
{{ $t("strings.daemon.remote.description") }}
</p>
<template v-if="config_daemon.type != 'remote'">
<div class="row pl-sm">
<LokiField class="col-8" :label="$t('fieldLabels.localDaemonIP')" disable>
<q-input
v-model="config_daemon.rpc_bind_ip"
:placeholder="daemon_defaults.rpc_bind_ip"
:dark="theme=='dark'"
disable
hide-underline
/>
</LokiField>
<LokiField class="col-4" :label="$t('fieldLabels.localDaemonPort') + '(RPC)'">
<q-input
v-model="config_daemon.rpc_bind_port"
:placeholder="toString(daemon_defaults.rpc_bind_port)"
type="number"
:decimals="0"
:step="1"
min="1024"
max="65535"
:dark="theme=='dark'"
hide-underline
/>
</LokiField>
</div>
<div class="row pl-sm">
<LokiField class="col-8" :label="$t('fieldLabels.localDaemonIP')" disable>
<q-input
v-model="config_daemon.rpc_bind_ip"
:placeholder="daemon_defaults.rpc_bind_ip"
:dark="theme == 'dark'"
disable
hide-underline
/>
</LokiField>
<LokiField class="col-4" :label="$t('fieldLabels.localDaemonPort') + '(RPC)'">
<q-input
v-model="config_daemon.rpc_bind_port"
:placeholder="toString(daemon_defaults.rpc_bind_port)"
type="number"
:decimals="0"
:step="1"
min="1024"
max="65535"
:dark="theme == 'dark'"
hide-underline
/>
</LokiField>
</div>
</template>
<template v-if="config_daemon.type != 'local'">
<div class="row q-mt-md pl-sm">
<LokiField class="col-8" :label="$t('fieldLabels.remoteNodeHost')">
<q-input
v-model="config_daemon.remote_host"
:placeholder="daemon_defaults.remote_host"
:dark="theme=='dark'"
hide-underline
/>
<!-- Remote node presets -->
<q-btn-dropdown class="remote-dropdown" v-if="config.app.net_type === 'mainnet'" flat>
<q-list link dark no-border>
<q-item v-for="option in remotes" :key="option.host" @click.native="setPreset(option)" v-close-overlay>
<q-item-main>
<q-item-tile label>{{ option.host }}:{{ option.port }}</q-item-tile>
</q-item-main>
</q-item>
</q-list>
</q-btn-dropdown>
</LokiField>
<LokiField class="col-4" :label="$t('fieldLabels.remoteNodePort')">
<q-input
v-model="config_daemon.remote_port"
:placeholder="toString(daemon_defaults.remote_port)"
type="number"
:decimals="0"
:step="1"
min="1024"
max="65535"
:dark="theme=='dark'"
hide-underline
/>
</LokiField>
</div>
<div class="row q-mt-md pl-sm">
<LokiField class="col-8" :label="$t('fieldLabels.remoteNodeHost')">
<q-input
v-model="config_daemon.remote_host"
:placeholder="daemon_defaults.remote_host"
:dark="theme == 'dark'"
hide-underline
/>
<!-- Remote node presets -->
<q-btn-dropdown v-if="config.app.net_type === 'mainnet'" class="remote-dropdown" flat>
<q-list link dark no-border>
<q-item v-for="option in remotes" :key="option.host" v-close-overlay @click.native="setPreset(option)">
<q-item-main>
<q-item-tile label>{{ option.host }}:{{ option.port }}</q-item-tile>
</q-item-main>
</q-item>
</q-list>
</q-btn-dropdown>
</LokiField>
<LokiField class="col-4" :label="$t('fieldLabels.remoteNodePort')">
<q-input
v-model="config_daemon.remote_port"
:placeholder="toString(daemon_defaults.remote_port)"
type="number"
:decimals="0"
:step="1"
min="1024"
max="65535"
:dark="theme == 'dark'"
hide-underline
/>
</LokiField>
</div>
</template>
<div class="col q-mt-md pt-sm">
<LokiField :label="$t('fieldLabels.dataStoragePath')" disable-hover>
<q-input v-model="config.app.data_dir" disable :dark="theme=='dark'" hide-underline/>
<input type="file" webkitdirectory directory id="dataPath" v-on:change="setDataPath" ref="fileInputData" hidden />
<q-btn color="secondary" v-on:click="selectPath('data')" :text-color="theme=='dark'?'white':'dark'">{{ $t("buttons.selectLocation") }}</q-btn>
</LokiField>
<LokiField :label="$t('fieldLabels.walletStoragePath')" disable-hover>
<q-input v-model="config.app.wallet_data_dir" disable :dark="theme=='dark'" hide-underline/>
<input type="file" webkitdirectory directory id="walletPath" v-on:change="setWalletDataPath" ref="fileInputWallet" hidden />
<q-btn color="secondary" v-on:click="selectPath('wallet')" :text-color="theme=='dark'?'white':'dark'">{{ $t("buttons.selectLocation") }}</q-btn>
</LokiField>
<LokiField :label="$t('fieldLabels.dataStoragePath')" disable-hover>
<q-input v-model="config.app.data_dir" disable :dark="theme == 'dark'" hide-underline />
<input id="dataPath" ref="fileInputData" type="file" webkitdirectory directory hidden @change="setDataPath" />
<q-btn color="secondary" :text-color="theme == 'dark' ? 'white' : 'dark'" @click="selectPath('data')">{{
$t("buttons.selectLocation")
}}</q-btn>
</LokiField>
<LokiField :label="$t('fieldLabels.walletStoragePath')" disable-hover>
<q-input v-model="config.app.wallet_data_dir" disable :dark="theme == 'dark'" hide-underline />
<input
id="walletPath"
ref="fileInputWallet"
type="file"
webkitdirectory
directory
hidden
@change="setWalletDataPath"
/>
<q-btn color="secondary" :text-color="theme == 'dark' ? 'white' : 'dark'" @click="selectPath('wallet')">{{
$t("buttons.selectLocation")
}}</q-btn>
</LokiField>
</div>
<q-collapsible :label="$t('strings.advancedOptions')" header-class="q-mt-sm non-selectable row reverse advanced-options-label">
<div class="row pl-sm q-mt-sm">
<LokiField class="col-6" :label="$t('fieldLabels.daemonLogLevel')" :disable="is_remote">
<q-input v-model="config_daemon.log_level" :placeholder="toString(daemon_defaults.log_level)" :disable="is_remote" :dark="theme=='dark'"
type="number" :decimals="0" :step="1" min="0" max="4" hide-underline />
</LokiField>
<LokiField class="col-6" :label="$t('fieldLabels.walletLogLevel')">
<q-input v-model="config.wallet.log_level" :placeholder="toString(defaults.wallet.log_level)" :dark="theme=='dark'"
type="number" :decimals="0" :step="1" min="0" max="4" hide-underline />
</LokiField>
</div>
<div class="row pl-sm q-mt-md">
<LokiField class="col-3" :label="$t('fieldLabels.maxIncomingPeers')" :disable="is_remote">
<q-input v-model="config_daemon.in_peers" :placeholder="toString(daemon_defaults.in_peers)" :disable="is_remote" :dark="theme=='dark'"
type="number" :decimals="0" :step="1" min="-1" max="65535" hide-underline />
</LokiField>
<LokiField class="col-3" :label="$t('fieldLabels.maxOutgoingPeers')" :disable="is_remote">
<q-input v-model="config_daemon.out_peers" :placeholder="toString(daemon_defaults.out_peers)" :disable="is_remote" :dark="theme=='dark'"
type="number" :decimals="0" :step="1" min="-1" max="65535" hide-underline />
</LokiField>
<LokiField class="col-3" :label="$t('fieldLabels.limitUploadRate')" :disable="is_remote">
<q-input v-model="config_daemon.limit_rate_up" :placeholder="toString(daemon_defaults.limit_rate_up)" :disable="is_remote" :dark="theme=='dark'"
type="number" suffix="Kb/s" :decimals="0" :step="1" min="-1" max="65535" hide-underline />
</LokiField>
<LokiField class="col-3" :label="$t('fieldLabels.limitDownloadRate')" :disable="is_remote">
<q-input v-model="config_daemon.limit_rate_down" :placeholder="toString(daemon_defaults.limit_rate_down)" :disable="is_remote" :dark="theme=='dark'"
type="number" suffix="Kb/s" :decimals="0" :step="1" min="-1" max="65535" hide-underline />
</LokiField>
</div>
<div class="row pl-sm q-mt-md">
<LokiField class="col-3" :label="$t('fieldLabels.daemonP2pPort')" :disable="is_remote">
<q-input v-model="config_daemon.p2p_bind_port" :placeholder="toString(daemon_defaults.p2p_bind_port)" :disable="is_remote" :dark="theme=='dark'"
float- type="number" :decimals="0" :step="1" min="1024" max="65535" hide-underline />
</LokiField>
<LokiField class="col-3" :label="$t('fieldLabels.daemonZMQPort')" :disable="is_remote">
<q-input v-model="config_daemon.zmq_rpc_bind_port" :placeholder="toString(daemon_defaults.zmq_rpc_bind_port)" :disable="is_remote" :dark="theme=='dark'"
float- type="number" :decimals="0" :step="1" min="1024" max="65535" hide-underline />
</LokiField>
<LokiField class="col-3" :label="$t('fieldLabels.internalWalletPort')">
<q-input v-model="config.app.ws_bind_port" :placeholder="toString(defaults.app.ws_bind_port)" :dark="theme=='dark'"
float- type="number" :decimals="0" :step="1" min="1024" max="65535" hide-underline />
</LokiField>
<LokiField class="col-3" :label="$t('fieldLabels.walletRPCPort')" :disable="is_remote">
<q-input v-model="config.wallet.rpc_bind_port" :placeholder="toString(defaults.wallet.rpc_bind_port)" :disable="is_remote" :dark="theme=='dark'"
float- type="number" :decimals="0" :step="1" min="1024" max="65535" hide-underline />
</LokiField>
</div>
<q-field :helper="$t('fieldLabels.chooseNetwork')" :label="$t('fieldLabels.network')" orientation="vertical">
<q-option-group
type="radio"
v-model="config.app.net_type"
:options="[
{ label: 'Main Net', value: 'mainnet' },
{ label: 'Stage Net', value: 'stagenet' },
{ label: 'Test Net', value: 'testnet' }
]"
/>
</q-field>
<q-collapsible
:label="$t('strings.advancedOptions')"
header-class="q-mt-sm non-selectable row reverse advanced-options-label"
>
<div class="row pl-sm q-mt-sm">
<LokiField class="col-6" :label="$t('fieldLabels.daemonLogLevel')" :disable="is_remote">
<q-input
v-model="config_daemon.log_level"
:placeholder="toString(daemon_defaults.log_level)"
:disable="is_remote"
:dark="theme == 'dark'"
type="number"
:decimals="0"
:step="1"
min="0"
max="4"
hide-underline
/>
</LokiField>
<LokiField class="col-6" :label="$t('fieldLabels.walletLogLevel')">
<q-input
v-model="config.wallet.log_level"
:placeholder="toString(defaults.wallet.log_level)"
:dark="theme == 'dark'"
type="number"
:decimals="0"
:step="1"
min="0"
max="4"
hide-underline
/>
</LokiField>
</div>
<div class="row pl-sm q-mt-md">
<LokiField class="col-3" :label="$t('fieldLabels.maxIncomingPeers')" :disable="is_remote">
<q-input
v-model="config_daemon.in_peers"
:placeholder="toString(daemon_defaults.in_peers)"
:disable="is_remote"
:dark="theme == 'dark'"
type="number"
:decimals="0"
:step="1"
min="-1"
max="65535"
hide-underline
/>
</LokiField>
<LokiField class="col-3" :label="$t('fieldLabels.maxOutgoingPeers')" :disable="is_remote">
<q-input
v-model="config_daemon.out_peers"
:placeholder="toString(daemon_defaults.out_peers)"
:disable="is_remote"
:dark="theme == 'dark'"
type="number"
:decimals="0"
:step="1"
min="-1"
max="65535"
hide-underline
/>
</LokiField>
<LokiField class="col-3" :label="$t('fieldLabels.limitUploadRate')" :disable="is_remote">
<q-input
v-model="config_daemon.limit_rate_up"
:placeholder="toString(daemon_defaults.limit_rate_up)"
:disable="is_remote"
:dark="theme == 'dark'"
type="number"
suffix="Kb/s"
:decimals="0"
:step="1"
min="-1"
max="65535"
hide-underline
/>
</LokiField>
<LokiField class="col-3" :label="$t('fieldLabels.limitDownloadRate')" :disable="is_remote">
<q-input
v-model="config_daemon.limit_rate_down"
:placeholder="toString(daemon_defaults.limit_rate_down)"
:disable="is_remote"
:dark="theme == 'dark'"
type="number"
suffix="Kb/s"
:decimals="0"
:step="1"
min="-1"
max="65535"
hide-underline
/>
</LokiField>
</div>
<div class="row pl-sm q-mt-md">
<LokiField class="col-3" :label="$t('fieldLabels.daemonP2pPort')" :disable="is_remote">
<q-input
v-model="config_daemon.p2p_bind_port"
:placeholder="toString(daemon_defaults.p2p_bind_port)"
:disable="is_remote"
:dark="theme == 'dark'"
float-
type="number"
:decimals="0"
:step="1"
min="1024"
max="65535"
hide-underline
/>
</LokiField>
<LokiField class="col-3" :label="$t('fieldLabels.daemonZMQPort')" :disable="is_remote">
<q-input
v-model="config_daemon.zmq_rpc_bind_port"
:placeholder="toString(daemon_defaults.zmq_rpc_bind_port)"
:disable="is_remote"
:dark="theme == 'dark'"
float-
type="number"
:decimals="0"
:step="1"
min="1024"
max="65535"
hide-underline
/>
</LokiField>
<LokiField class="col-3" :label="$t('fieldLabels.internalWalletPort')">
<q-input
v-model="config.app.ws_bind_port"
:placeholder="toString(defaults.app.ws_bind_port)"
:dark="theme == 'dark'"
float-
type="number"
:decimals="0"
:step="1"
min="1024"
max="65535"
hide-underline
/>
</LokiField>
<LokiField class="col-3" :label="$t('fieldLabels.walletRPCPort')" :disable="is_remote">
<q-input
v-model="config.wallet.rpc_bind_port"
:placeholder="toString(defaults.wallet.rpc_bind_port)"
:disable="is_remote"
:dark="theme == 'dark'"
float-
type="number"
:decimals="0"
:step="1"
min="1024"
max="65535"
hide-underline
/>
</LokiField>
</div>
<q-field :helper="$t('fieldLabels.chooseNetwork')" :label="$t('fieldLabels.network')" orientation="vertical">
<q-option-group
v-model="config.app.net_type"
type="radio"
:options="[
{ label: 'Main Net', value: 'mainnet' },
{ label: 'Stage Net', value: 'stagenet' },
{ label: 'Test Net', value: 'testnet' }
]"
/>
</q-field>
</q-collapsible>
</div>
</div>
</template>
<script>
import { mapState } from "vuex"
import LokiField from "components/loki_field"
import { mapState } from "vuex";
import LokiField from "components/loki_field";
export default {
name: "SettingsGeneral",
props: {
randomise_remote: {
type: Boolean,
required: false,
default: false,
},
},
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
remotes: state => state.gateway.app.remotes,
config: state => state.gateway.app.pending_config,
config_daemon (state) {
return this.config.daemons[this.config.app.net_type]
},
is_remote (state) {
return this.config_daemon.type === 'remote'
},
defaults: state => state.gateway.app.defaults,
daemon_defaults (state) {
return this.defaults.daemons[this.config.app.net_type]
}
}),
mounted () {
if(this.randomise_remote && this.remotes.length > 0 && this.config.app.net_type === "mainnet") {
const index = Math.floor(Math.random() * Math.floor(this.remotes.length));
this.setPreset(this.remotes[index]);
}
},
methods: {
selectPath (type) {
const fileInput = type === "data" ? "fileInputData" : "fileInputWallet"
this.$refs[fileInput].click()
},
setDataPath (file) {
if (file.target.files && file.target.files.length > 0) {
this.config.app.data_dir = file.target.files[0].path
}
},
setWalletDataPath (file) {
if (file.target.files && file.target.files.length > 0) {
this.config.app.wallet_data_dir = file.target.files[0].path
}
},
setPreset (option) {
if (!option) return;
const { host, port } = option;
if (host) this.config_daemon.remote_host = host;
if (port) this.config_daemon.remote_port = port;
},
toString (value) {
if (!value && typeof value !== "number") return ""
return String(value);
}
},
data () {
return {
select: 0,
}
},
components: {
LokiField,
name: "SettingsGeneral",
components: {
LokiField
},
props: {
randomiseRemote: {
type: Boolean,
required: false,
default: false
}
}
},
data() {
return {
select: 0
};
},
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
remotes: state => state.gateway.app.remotes,
config: state => state.gateway.app.pending_config,
config_daemon() {
return this.config.daemons[this.config.app.net_type];
},
is_remote() {
return this.config_daemon.type === "remote";
},
defaults: state => state.gateway.app.defaults,
daemon_defaults() {
return this.defaults.daemons[this.config.app.net_type];
}
}),
mounted() {
if (this.randomise_remote && this.remotes.length > 0 && this.config.app.net_type === "mainnet") {
const index = Math.floor(Math.random() * Math.floor(this.remotes.length));
this.setPreset(this.remotes[index]);
}
},
methods: {
selectPath(type) {
const fileInput = type === "data" ? "fileInputData" : "fileInputWallet";
this.$refs[fileInput].click();
},
setDataPath(file) {
if (file.target.files && file.target.files.length > 0) {
this.config.app.data_dir = file.target.files[0].path;
}
},
setWalletDataPath(file) {
if (file.target.files && file.target.files.length > 0) {
this.config.app.wallet_data_dir = file.target.files[0].path;
}
},
setPreset(option) {
if (!option) return;
const { host, port } = option;
if (host) this.config_daemon.remote_host = host;
if (port) this.config_daemon.remote_port = port;
},
toString(value) {
if (!value && typeof value !== "number") return "";
return String(value);
}
}
};
</script>
<style lang="scss">
.settings-general {
.q-field {
margin: 20px 0
}
.q-field {
margin: 20px 0;
}
.q-if-disabled {
cursor: default !important;
.q-input-target {
cursor: default !important;
}
.q-if-disabled {
cursor: default !important;
.q-input-target {
cursor: default !important;
}
}
.q-item,
.q-collapsible-sub-item {
padding: 0;
}
.q-item,
.q-collapsible-sub-item {
padding: 0;
}
.row.pl-sm {
> * + * {
padding-left: 16px;
}
.row.pl-sm {
> * + * {
padding-left: 16px;
}
}
.col.pt-sm {
> * + * {
padding-top: 16px;
}
.col.pt-sm {
> * + * {
padding-top: 16px;
}
}
.remote-dropdown {
padding: 0 !important;
}
.remote-dropdown {
padding: 0 !important;
}
}
</style>

View File

@ -1,301 +1,344 @@
<template>
<q-modal v-model="isVisible" maximized>
<q-modal v-model="isVisible" maximized>
<q-modal-layout>
<q-toolbar slot="header" color="dark" inverted>
<q-btn
flat
round
dense
@click="isVisible = false"
icon="reply"
/>
<q-toolbar-title>
{{ $t("titles.transactionDetails") }}
</q-toolbar-title>
<q-btn flat class="q-mr-sm" @click="showTxDetails" :label="$t('buttons.showTxDetails')" />
<q-btn v-if="can_open" color="primary" @click="openExplorer" :label="$t('buttons.viewOnExplorer')" />
</q-toolbar>
<q-toolbar slot="header" color="dark" inverted>
<q-btn flat round dense icon="reply" @click="isVisible = false" />
<q-toolbar-title>
{{ $t("titles.transactionDetails") }}
</q-toolbar-title>
<q-btn flat class="q-mr-sm" :label="$t('buttons.showTxDetails')" @click="showTxDetails" />
<q-btn v-if="can_open" color="primary" :label="$t('buttons.viewOnExplorer')" @click="openExplorer" />
</q-toolbar>
<div class="layout-padding">
<div class="row items-center non-selectable">
<div class="q-mr-sm">
<TxTypeIcon :type="tx.type" :tooltip="false" />
</div>
<div :class="'tx-'+tx.type" v-if="tx.type=='in'">
{{ $t("strings.transactions.description", { type: $t("strings.transactions.types.incoming") }) }}
</div>
<div :class="'tx-'+tx.type" v-else-if="tx.type=='out'">
{{ $t("strings.transactions.description", { type: $t("strings.transactions.types.outgoing") }) }}
</div>
<div :class="'tx-'+tx.type" v-else-if="tx.type=='pool'">
{{ $t("strings.transactions.description", { type: $t("strings.transactions.types.pendingIncoming") }) }}
</div>
<div :class="'tx-'+tx.type" v-else-if="tx.type=='pending'">
{{ $t("strings.transactions.description", { type: $t("strings.transactions.types.pendingOutgoing") }) }}
</div>
<div :class="'tx-'+tx.type" v-else-if="tx.type=='failed'">
{{ $t("strings.transactions.description", { type: $t("strings.transactions.types.failed") }) }}
</div>
</div>
<div class="row justify-between" style="max-width: 768px">
<div class="infoBox">
<div class="infoBoxContent">
<div class="text"><span>{{ $t("strings.transactions.amount") }}</span></div>
<div class="value"><span><FormatLoki :amount="tx.amount" raw-value /></span></div>
</div>
</div>
<div class="infoBox">
<div class="infoBoxContent">
<div class="text">
<span>
{{ $t("strings.transactions.fee") }}
<template v-if="tx.type=='in'||tx.type=='pool'">
({{ $t("strings.transactions.paidBySender") }})
</template>
</span>
</div>
<div class="value"><span><FormatLoki :amount="tx.fee" raw-value /></span></div>
</div>
</div>
<div class="infoBox">
<div class="infoBoxContent">
<div class="text"><span>{{ $t("strings.blockHeight") }}</span></div>
<div class="value"><span>{{ tx.height }}</span></div>
</div>
</div>
<div class="infoBox">
<div class="infoBoxContent">
<div class="text"><span>{{ $t("strings.transactions.timestamp") }}</span></div>
<div class="value"><span>{{ formatDate(tx.timestamp*1000) }}</span></div>
</div>
</div>
</div>
<h6 class="q-mt-xs q-mb-none text-weight-light">{{ $t("strings.transactionID") }}</h6>
<p class="monospace break-all">{{ tx.txid }}</p>
<h6 class="q-mt-xs q-mb-none text-weight-light">{{ $t("strings.paymentID") }}</h6>
<p class="monospace break-all">{{ tx.payment_id ? tx.payment_id : 'N/A' }}</p>
<div v-if="tx.type=='in' || tx.type=='pool'">
<q-list no-border>
<q-list-header class="q-px-none">
{{ $t("strings.transactions.sentTo", { type: $t("strings.transactions.types.incoming") }) }}:
</q-list-header>
<q-item class="q-px-none">
<q-item-main>
<q-item-tile label class="non-selectable">{{ in_tx_address_used.address_index_text }}</q-item-tile>
<q-item-tile class="monospace ellipsis" sublabel>{{ in_tx_address_used.address }}</q-item-tile>
</q-item-main>
<q-context-menu>
<q-list link separator style="min-width: 150px; max-height: 300px;">
<q-item v-close-overlay
@click.native="copyAddress(in_tx_address_used.address, $event)">
<q-item-main :label="$t('menuItems.copyAddress')" />
</q-item>
</q-list>
</q-context-menu>
</q-item>
</q-list>
</div>
<div v-else-if="tx.type=='out' || tx.type=='pending'">
<q-list no-border>
<q-list-header class="q-px-none">
{{ $t("strings.transactions.sentTo", { type: $t("strings.transactions.types.outgoing") }) }}:
</q-list-header>
<template v-if="out_destinations">
<q-item class="q-px-none" v-for="destination in out_destinations">
<q-item-main>
<q-item-tile label>{{ destination.name }}</q-item-tile>
<q-item-tile class="monospace ellipsis" sublabel>{{ destination.address }}</q-item-tile>
<q-item-tile sublabel><FormatLoki :amount="destination.amount" /></q-item-tile>
</q-item-main>
<q-context-menu>
<q-list link separator style="min-width: 150px; max-height: 300px;">
<q-item v-close-overlay
@click.native="copyAddress(destination.address, $event)">
<q-item-main :label="$t('menuItems.copyAddress')" />
</q-item>
</q-list>
</q-context-menu>
</q-item>
</template>
<template v-else>
<q-item class="q-px-none">
<q-item-main>
<q-item-tile label>{{ $t('strings.destinationUnknown') }}</q-item-tile>
</q-item-main>
</q-item>
</template>
</q-list>
</div>
<q-field class="q-mt-md">
<q-input
v-model="txNotes" :float-label="$t('fieldLabels.transactionNotes')"
:dark="theme=='dark'"
type="textarea" rows="2" />
</q-field>
<q-field class="q-mt-sm">
<q-btn
:disable="!is_ready"
:text-color="theme=='dark'?'white':'dark'"
@click="saveTxNotes" :label="$t('buttons.saveTxNotes')" />
</q-field>
<div class="layout-padding">
<div class="row items-center non-selectable">
<div class="q-mr-sm">
<TxTypeIcon :type="tx.type" :tooltip="false" />
</div>
<div v-if="tx.type == 'in'" :class="'tx-' + tx.type">
{{
$t("strings.transactions.description", {
type: $t("strings.transactions.types.incoming")
})
}}
</div>
<div v-else-if="tx.type == 'out'" :class="'tx-' + tx.type">
{{
$t("strings.transactions.description", {
type: $t("strings.transactions.types.outgoing")
})
}}
</div>
<div v-else-if="tx.type == 'pool'" :class="'tx-' + tx.type">
{{
$t("strings.transactions.description", {
type: $t("strings.transactions.types.pendingIncoming")
})
}}
</div>
<div v-else-if="tx.type == 'pending'" :class="'tx-' + tx.type">
{{
$t("strings.transactions.description", {
type: $t("strings.transactions.types.pendingOutgoing")
})
}}
</div>
<div v-else-if="tx.type == 'failed'" :class="'tx-' + tx.type">
{{
$t("strings.transactions.description", {
type: $t("strings.transactions.types.failed")
})
}}
</div>
</div>
</q-modal-layout>
<div class="row justify-between" style="max-width: 768px">
<div class="infoBox">
<div class="infoBoxContent">
<div class="text">
<span>{{ $t("strings.transactions.amount") }}</span>
</div>
<div class="value">
<span><FormatLoki :amount="tx.amount" raw-value/></span>
</div>
</div>
</div>
</q-modal>
<div class="infoBox">
<div class="infoBoxContent">
<div class="text">
<span>
{{ $t("strings.transactions.fee") }}
<template v-if="tx.type == 'in' || tx.type == 'pool'">
({{ $t("strings.transactions.paidBySender") }})
</template>
</span>
</div>
<div class="value">
<span><FormatLoki :amount="tx.fee" raw-value/></span>
</div>
</div>
</div>
<div class="infoBox">
<div class="infoBoxContent">
<div class="text">
<span>{{ $t("strings.blockHeight") }}</span>
</div>
<div class="value">
<span>{{ tx.height }}</span>
</div>
</div>
</div>
<div class="infoBox">
<div class="infoBoxContent">
<div class="text">
<span>{{ $t("strings.transactions.timestamp") }}</span>
</div>
<div class="value">
<span>{{ formatDate(tx.timestamp * 1000) }}</span>
</div>
</div>
</div>
</div>
<h6 class="q-mt-xs q-mb-none text-weight-light">
{{ $t("strings.transactionID") }}
</h6>
<p class="monospace break-all">{{ tx.txid }}</p>
<h6 class="q-mt-xs q-mb-none text-weight-light">
{{ $t("strings.paymentID") }}
</h6>
<p class="monospace break-all">
{{ tx.payment_id ? tx.payment_id : "N/A" }}
</p>
<div v-if="tx.type == 'in' || tx.type == 'pool'">
<q-list no-border>
<q-list-header class="q-px-none">
{{
$t("strings.transactions.sentTo", {
type: $t("strings.transactions.types.incoming")
})
}}:
</q-list-header>
<q-item class="q-px-none">
<q-item-main>
<q-item-tile label class="non-selectable">{{ in_tx_address_used.address_index_text }}</q-item-tile>
<q-item-tile class="monospace ellipsis" sublabel>{{ in_tx_address_used.address }}</q-item-tile>
</q-item-main>
<q-context-menu>
<q-list link separator style="min-width: 150px; max-height: 300px;">
<q-item v-close-overlay @click.native="copyAddress(in_tx_address_used.address, $event)">
<q-item-main :label="$t('menuItems.copyAddress')" />
</q-item>
</q-list>
</q-context-menu>
</q-item>
</q-list>
</div>
<div v-else-if="tx.type == 'out' || tx.type == 'pending'">
<q-list no-border>
<q-list-header class="q-px-none">
{{
$t("strings.transactions.sentTo", {
type: $t("strings.transactions.types.outgoing")
})
}}:
</q-list-header>
<template v-if="out_destinations">
<q-item v-for="destination in out_destinations" :key="destination.address" class="q-px-none">
<q-item-main>
<q-item-tile label>{{ destination.name }}</q-item-tile>
<q-item-tile class="monospace ellipsis" sublabel>{{ destination.address }}</q-item-tile>
<q-item-tile sublabel><FormatLoki :amount="destination.amount"/></q-item-tile>
</q-item-main>
<q-context-menu>
<q-list link separator style="min-width: 150px; max-height: 300px;">
<q-item v-close-overlay @click.native="copyAddress(destination.address, $event)">
<q-item-main :label="$t('menuItems.copyAddress')" />
</q-item>
</q-list>
</q-context-menu>
</q-item>
</template>
<template v-else>
<q-item class="q-px-none">
<q-item-main>
<q-item-tile label>{{ $t("strings.destinationUnknown") }}</q-item-tile>
</q-item-main>
</q-item>
</template>
</q-list>
</div>
<q-field class="q-mt-md">
<q-input
v-model="txNotes"
:float-label="$t('fieldLabels.transactionNotes')"
:dark="theme == 'dark'"
type="textarea"
rows="2"
/>
</q-field>
<q-field class="q-mt-sm">
<q-btn
:disable="!is_ready"
:text-color="theme == 'dark' ? 'white' : 'dark'"
:label="$t('buttons.saveTxNotes')"
@click="saveTxNotes"
/>
</q-field>
</div>
</q-modal-layout>
</q-modal>
</template>
<script>
const { clipboard } = require("electron")
import { mapState } from "vuex"
import { date } from "quasar"
const { formatDate } = date
import TxTypeIcon from "components/tx_type_icon"
import FormatLoki from "components/format_loki"
const { clipboard } = require("electron");
import { mapState } from "vuex";
import { date } from "quasar";
import TxTypeIcon from "components/tx_type_icon";
import FormatLoki from "components/format_loki";
export default {
name: "TxDetails",
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
can_open (state) {
const { net_type } = state.gateway.app.config.app
return net_type !== "stagenet"
},
in_tx_address_used (state) {
let i
let used_addresses = state.gateway.wallet.address_list.primary.concat(state.gateway.wallet.address_list.used)
for(i=0; i < used_addresses.length; i++) {
if(used_addresses[i].address_index == this.tx.subaddr_index.minor) {
let address_index_text = ""
if(used_addresses[i].address_index === 0) {
address_index_text = this.$t("strings.addresses.primaryAddress")
} else {
address_index_text = this.$t("strings.addresses.subAddress") + " (" + this.$t('strings.addresses.subAddressIndex', { index: used_addresses[i].address_index}) + ")"
}
return {
address: used_addresses[i].address,
address_index: used_addresses[i].address_index,
address_index_text: address_index_text
}
}
}
return false
},
out_destinations (state) {
if(!this.tx.destinations)
return false
let i, j
let destinations = []
let address_book = state.gateway.wallet.address_list.address_book
for(i=0; i < this.tx.destinations.length; i++) {
let destination = this.tx.destinations[i]
destination.name = ""
for(j=0; j < address_book.length; j++) {
if(destination.address == address_book[j].address) {
const { name, description} = address_book[j]
const separator = description === "" ? "" : " - "
destination.name = `${name}${separator}${description}`
break;
}
}
destinations.push(destination)
}
return destinations
},
is_ready (state) {
return this.$store.getters["gateway/isReady"]
}
}),
data () {
return {
isVisible: false,
txNotes: "",
tx: {
address: "",
amount: 0,
double_spend_seen: false,
fee: 0,
height: 0,
note: "",
payment_id: "",
subaddr_index: {major: 0, minor: 0},
timestamp: 0,
txid: "",
type: "",
unlock_time:0
}
}
name: "TxDetails",
components: {
TxTypeIcon,
FormatLoki
},
data() {
return {
isVisible: false,
txNotes: "",
tx: {
address: "",
amount: 0,
double_spend_seen: false,
fee: 0,
height: 0,
note: "",
payment_id: "",
subaddr_index: { major: 0, minor: 0 },
timestamp: 0,
txid: "",
type: "",
unlock_time: 0
}
};
},
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
can_open(state) {
const { net_type } = state.gateway.app.config.app;
return net_type !== "stagenet";
},
methods: {
showTxDetails () {
this.$q.dialog({
title: this.$t("dialog.transactionDetails.title"),
message: JSON.stringify(this.tx, null, 2),
ok: {
label: this.$t("dialog.transactionDetails.ok"),
color: "primary",
},
}).then(() => {
}).catch(() => {
});
},
openExplorer () {
this.$gateway.send("core", "open_explorer", {type: "tx", id: this.tx.txid})
},
saveTxNotes () {
this.$q.notify({
timeout: 1000,
type: "positive",
message: this.$t("notification.positive.transactionNotesSaved")
})
this.$gateway.send("wallet", "save_tx_notes", {txid: this.tx.txid, note: this.txNotes})
},
formatDate (timestamp) {
return date.formatDate(timestamp, "YYYY-MM-DD hh:mm a")
},
copyAddress (address, event) {
event.stopPropagation()
for(let i = 0; i < event.path.length; i++) {
if(event.path[i].tagName == "BUTTON") {
event.path[i].blur()
break
}
}
clipboard.writeText(address)
this.$q.notify({
type: "positive",
timeout: 1000,
message: this.$t("notification.positive.addressCopied")
})
in_tx_address_used(state) {
let i;
let used_addresses = state.gateway.wallet.address_list.primary.concat(state.gateway.wallet.address_list.used);
for (i = 0; i < used_addresses.length; i++) {
if (used_addresses[i].address_index == this.tx.subaddr_index.minor) {
let address_index_text = "";
if (used_addresses[i].address_index === 0) {
address_index_text = this.$t("strings.addresses.primaryAddress");
} else {
address_index_text =
this.$t("strings.addresses.subAddress") +
" (" +
this.$t("strings.addresses.subAddressIndex", {
index: used_addresses[i].address_index
}) +
")";
}
return {
address: used_addresses[i].address,
address_index: used_addresses[i].address_index,
address_index_text: address_index_text
};
}
}
return false;
},
components: {
TxTypeIcon,
FormatLoki
out_destinations(state) {
if (!this.tx.destinations) return false;
let i, j;
let destinations = [];
let address_book = state.gateway.wallet.address_list.address_book;
for (i = 0; i < this.tx.destinations.length; i++) {
let destination = this.tx.destinations[i];
destination.name = "";
for (j = 0; j < address_book.length; j++) {
if (destination.address == address_book[j].address) {
const { name, description } = address_book[j];
const separator = description === "" ? "" : " - ";
destination.name = `${name}${separator}${description}`;
break;
}
}
destinations.push(destination);
}
return destinations;
},
is_ready() {
return this.$store.getters["gateway/isReady"];
}
}
}),
methods: {
showTxDetails() {
this.$q
.dialog({
title: this.$t("dialog.transactionDetails.title"),
message: JSON.stringify(this.tx, null, 2),
ok: {
label: this.$t("dialog.transactionDetails.ok"),
color: "primary"
}
})
.then(() => {})
.catch(() => {});
},
openExplorer() {
this.$gateway.send("core", "open_explorer", {
type: "tx",
id: this.tx.txid
});
},
saveTxNotes() {
this.$q.notify({
timeout: 1000,
type: "positive",
message: this.$t("notification.positive.transactionNotesSaved")
});
this.$gateway.send("wallet", "save_tx_notes", {
txid: this.tx.txid,
note: this.txNotes
});
},
formatDate(timestamp) {
return date.formatDate(timestamp, "YYYY-MM-DD hh:mm a");
},
copyAddress(address, event) {
event.stopPropagation();
for (let i = 0; i < event.path.length; i++) {
if (event.path[i].tagName == "BUTTON") {
event.path[i].blur();
break;
}
}
clipboard.writeText(address);
this.$q.notify({
type: "positive",
timeout: 1000,
message: this.$t("notification.positive.addressCopied")
});
}
}
};
</script>
<style>
</style>
<style></style>

View File

@ -1,335 +1,339 @@
<template>
<div class="tx-list">
<div class="tx-list">
<template v-if="tx_list_paged.length === 0">
<p class="q-pa-md q-mb-none">{{ $t("strings.noTransactionsFound") }}</p>
<p class="q-pa-md q-mb-none">
{{ $t("strings.noTransactionsFound") }}
</p>
</template>
<template v-else>
<q-infinite-scroll :handler="loadMore" ref="scroller">
<q-list link no-border :dark="theme=='dark'" class="loki-list tx-list">
<q-item class="loki-list-item transaction" v-for="(tx, index) in tx_list_paged" :key="`${tx.txid}-${tx.type}`"
@click.native="details(tx)" :class="'tx-'+tx.type">
<q-item-side class="type">
<div>{{ tx.type | typeToString }}</div>
</q-item-side>
<q-item-main class="main">
<q-item-tile class="amount" label>
<FormatLoki :amount="tx.amount" />
</q-item-tile>
<q-item-tile sublabel>{{ tx.txid }}</q-item-tile>
</q-item-main>
<q-item-side class="meta">
<q-item-tile label>
<timeago :datetime="tx.timestamp*1000" :auto-update="60" :locale="$i18n.locale"/>
</q-item-tile>
<q-item-tile sublabel>{{ formatHeight(tx) }}</q-item-tile>
</q-item-side>
<q-context-menu>
<q-list link separator style="min-width: 150px; max-height: 300px;">
<q-item v-close-overlay
@click.native="details(tx)">
<q-item-main :label="$t('menuItems.showDetails')" />
</q-item>
<q-item v-close-overlay
@click.native="copyTxid(tx.txid, $event)">
<q-item-main :label="$t('menuItems.copyTransactionId')" />
</q-item>
<q-item v-close-overlay
@click.native="openExplorer(tx.txid)">
<q-item-main :label="$t('menuItems.viewOnExplorer')" />
</q-item>
</q-list>
</q-context-menu>
<q-infinite-scroll ref="scroller" :handler="loadMore">
<q-list link no-border :dark="theme == 'dark'" class="loki-list tx-list">
<q-item
v-for="tx in tx_list_paged"
:key="`${tx.txid}-${tx.type}`"
class="loki-list-item transaction"
:class="'tx-' + tx.type"
@click.native="details(tx)"
>
<q-item-side class="type">
<div>{{ tx.type | typeToString }}</div>
</q-item-side>
<q-item-main class="main">
<q-item-tile class="amount" label>
<FormatLoki :amount="tx.amount" />
</q-item-tile>
<q-item-tile sublabel>{{ tx.txid }}</q-item-tile>
</q-item-main>
<q-item-side class="meta">
<q-item-tile label>
<timeago :datetime="tx.timestamp * 1000" :auto-update="60" :locale="$i18n.locale" />
</q-item-tile>
<q-item-tile sublabel>{{ formatHeight(tx) }}</q-item-tile>
</q-item-side>
<q-context-menu>
<q-list link separator style="min-width: 150px; max-height: 300px;">
<q-item v-close-overlay @click.native="details(tx)">
<q-item-main :label="$t('menuItems.showDetails')" />
</q-item>
<q-spinner-dots slot="message" :size="40"></q-spinner-dots>
</q-list>
</q-infinite-scroll>
<q-item v-close-overlay @click.native="copyTxid(tx.txid, $event)">
<q-item-main :label="$t('menuItems.copyTransactionId')" />
</q-item>
<q-item v-close-overlay @click.native="openExplorer(tx.txid)">
<q-item-main :label="$t('menuItems.viewOnExplorer')" />
</q-item>
</q-list>
</q-context-menu>
</q-item>
<QSpinnerDots slot="message" :size="40"></QSpinnerDots>
</q-list>
</q-infinite-scroll>
</template>
<TxDetails ref="txDetails" />
</div>
</div>
</template>
<script>
const { clipboard } = require("electron")
import { mapState } from "vuex"
import { QSpinnerDots } from "quasar"
import Identicon from "components/identicon"
import TxTypeIcon from "components/tx_type_icon"
import TxDetails from "components/tx_details"
import FormatLoki from "components/format_loki"
import { i18n } from "plugins/i18n"
const { clipboard } = require("electron");
import { mapState } from "vuex";
import { QSpinnerDots } from "quasar";
import TxDetails from "components/tx_details";
import FormatLoki from "components/format_loki";
import { i18n } from "plugins/i18n";
export default {
name: "TxList",
props: {
limit: {
type: Number,
required: false,
default: -1
},
type: {
type: String,
required: false,
default: "all"
},
filter: {
type: String,
required: false,
default: ""
},
toOutgoingAddress: {
type: String,
required: false,
default: ""
},
toIncomingAddressIndex: {
type: Number,
required: false,
default: -1
},
},
data () {
return {
page: 0,
tx_list_filtered: [],
tx_list_paged: []
}
},
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
current_height: state => state.gateway.daemon.info.height,
wallet_height: state => state.gateway.wallet.info.height,
tx_list: state => state.gateway.wallet.transactions.tx_list,
address_book: state => state.gateway.wallet.address_list.address_book
}),
created () {
this.filterTxList()
this.pageTxList()
},
watch: {
wallet_height: {
handler(val, old){
if(val == old) return
this.filterTxList()
this.pageTxList()
}
},
tx_list: {
handler(val, old ) {
// Check if anything changed in the tx list
if(val.length == old.length) {
const changed = val.filter((v, i) => v.note !== old[i].note)
if (changed.length === 0) return
}
this.filterTxList()
this.pageTxList()
}
},
type: {
handler(val, old){
if(val == old) return
if(this.$refs.scroller) {
this.$refs.scroller.stop()
this.page = 0
this.$refs.scroller.reset()
this.$refs.scroller.resume()
}
this.filterTxList()
this.pageTxList()
}
},
filter: {
handler(val, old){
if(val == old) return
if(this.$refs.scroller) {
this.$refs.scroller.stop()
this.page = 0
this.$refs.scroller.reset()
this.$refs.scroller.resume()
}
this.filterTxList()
this.pageTxList()
}
},
},
filters: {
typeToString: function (value) {
switch (value) {
case "in":
return i18n.t("strings.transactions.received")
case "out":
return i18n.t("strings.transactions.sent")
case "failed":
return i18n.t("strings.transactions.types.failed")
case "pending":
case "pool":
return i18n.t("strings.transactions.types.pending")
case "miner":
return i18n.t("strings.transactions.types.miner")
case "snode":
return i18n.t("strings.transactions.types.serviceNode")
case "gov":
return i18n.t("strings.transactions.types.governance")
case "stake":
return i18n.t("strings.transactions.types.stake")
default:
return "-"
}
}
},
methods: {
filterTxList () {
const all_in = ["in", "pool", "miner", "snode", "gov"]
const all_out = ["out", "pending", "stake"]
const all_pending = ["pending", "pool"]
this.tx_list_filtered = this.tx_list.filter((tx) => {
let valid = true
if (this.type === "all_in" && !all_in.includes(tx.type)) {
return false
}
if (this.type === "all_out" && !all_out.includes(tx.type)) {
return false
}
if (this.type === "all_pending" && !all_pending.includes(tx.type)) {
return false
}
if(!this.type.startsWith("all") && this.type !== tx.type) {
valid = false
return valid
}
if(this.filter !== "") {
valid = this.txContains(tx, this.filter)
return valid
}
if(this.toOutgoingAddress !== "") {
if(tx.hasOwnProperty("destinations")) {
valid = tx.destinations.filter((destination) => { return destination.address === this.toOutgoingAddress }).length;
} else {
valid = false
}
return valid
}
if(this.toIncomingAddressIndex !== -1) {
valid = tx.hasOwnProperty("subaddr_index") && tx.subaddr_index.minor == this.toIncomingAddressIndex
return valid
}
return valid
})
},
txContains(tx, value) {
// The tx can be searchable using:
// id, address, notes, amount, recipient name
const fields = [tx.txid, tx.note]
const formattedAmount = tx.amount / 1e9
fields.push(String(formattedAmount))
// Get all addresses and names and add them on
const destinations = (tx.destinations || []).map(d => d.address)
const addresses = [tx.address, ...destinations]
const contacts = addresses.map(this.getContact).filter(c => !!c).map(c => c.name)
fields.push(...addresses, ...contacts)
return !!fields.find(f => f.toLowerCase().includes(value.toLowerCase()))
},
getContact(address) {
return this.address_book.find(book => book.address === address)
},
pageTxList () {
this.tx_list_paged = this.tx_list_filtered.slice(0, this.limit !== -1 ? this.limit : this.page * 24 + 24)
},
loadMore: function(index, done) {
this.page = index
if(this.limit !== -1 || this.tx_list_filtered.length < this.page * 24 + 24) {
this.$refs.scroller.stop()
}
this.pageTxList()
this.$nextTick(() => {
done()
})
},
details (tx) {
this.$refs.txDetails.tx = tx;
this.$refs.txDetails.txNotes = tx.note;
this.$refs.txDetails.isVisible = true;
},
formatHeight(tx) {
let height = tx.height;
let confirms = Math.max(0, this.wallet_height - height);
if(height == 0)
return this.$t("strings.transactions.types.pending")
if(confirms < Math.max(10, tx.unlock_time - height))
return this.$t("strings.blockHeight") + `: ${height} (${confirms} confirm${confirms==1?'':'s'})`
else
return this.$t("strings.blockHeight") + `: ${height} (${this.$t("strings.transactionConfirmed")})`
},
copyTxid (txid, event) {
event.stopPropagation()
for(let i = 0; i < event.path.length; i++) {
if(event.path[i].tagName == "BUTTON") {
event.path[i].blur()
break
}
}
clipboard.writeText(txid)
this.$q.notify({
type: "positive",
timeout: 1000,
message: this.$t("notification.positive.copied", { item: "Txid" })
})
},
openExplorer (txid) {
this.$gateway.send("core", "open_explorer", {type: "tx", id: txid})
}
},
components: {
QSpinnerDots,
Identicon,
TxTypeIcon,
TxDetails,
FormatLoki
name: "TxList",
filters: {
typeToString: function(value) {
switch (value) {
case "in":
return i18n.t("strings.transactions.received");
case "out":
return i18n.t("strings.transactions.sent");
case "failed":
return i18n.t("strings.transactions.types.failed");
case "pending":
case "pool":
return i18n.t("strings.transactions.types.pending");
case "miner":
return i18n.t("strings.transactions.types.miner");
case "snode":
return i18n.t("strings.transactions.types.serviceNode");
case "gov":
return i18n.t("strings.transactions.types.governance");
case "stake":
return i18n.t("strings.transactions.types.stake");
default:
return "-";
}
}
}
},
components: {
QSpinnerDots,
TxDetails,
FormatLoki
},
props: {
limit: {
type: Number,
required: false,
default: -1
},
type: {
type: String,
required: false,
default: "all"
},
filter: {
type: String,
required: false,
default: ""
},
toOutgoingAddress: {
type: String,
required: false,
default: ""
},
toIncomingAddressIndex: {
type: Number,
required: false,
default: -1
}
},
data() {
return {
page: 0,
tx_list_filtered: [],
tx_list_paged: []
};
},
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
current_height: state => state.gateway.daemon.info.height,
wallet_height: state => state.gateway.wallet.info.height,
tx_list: state => state.gateway.wallet.transactions.tx_list,
address_book: state => state.gateway.wallet.address_list.address_book
}),
watch: {
wallet_height: {
handler(val, old) {
if (val == old) return;
this.filterTxList();
this.pageTxList();
}
},
tx_list: {
handler(val, old) {
// Check if anything changed in the tx list
if (val.length == old.length) {
const changed = val.filter((v, i) => v.note !== old[i].note);
if (changed.length === 0) return;
}
this.filterTxList();
this.pageTxList();
}
},
type: {
handler(val, old) {
if (val == old) return;
if (this.$refs.scroller) {
this.$refs.scroller.stop();
this.page = 0;
this.$refs.scroller.reset();
this.$refs.scroller.resume();
}
this.filterTxList();
this.pageTxList();
}
},
filter: {
handler(val, old) {
if (val == old) return;
if (this.$refs.scroller) {
this.$refs.scroller.stop();
this.page = 0;
this.$refs.scroller.reset();
this.$refs.scroller.resume();
}
this.filterTxList();
this.pageTxList();
}
}
},
created() {
this.filterTxList();
this.pageTxList();
},
methods: {
filterTxList() {
const all_in = ["in", "pool", "miner", "snode", "gov"];
const all_out = ["out", "pending", "stake"];
const all_pending = ["pending", "pool"];
this.tx_list_filtered = this.tx_list.filter(tx => {
let valid = true;
if (this.type === "all_in" && !all_in.includes(tx.type)) {
return false;
}
if (this.type === "all_out" && !all_out.includes(tx.type)) {
return false;
}
if (this.type === "all_pending" && !all_pending.includes(tx.type)) {
return false;
}
if (!this.type.startsWith("all") && this.type !== tx.type) {
valid = false;
return valid;
}
if (this.filter !== "") {
valid = this.txContains(tx, this.filter);
return valid;
}
if (this.toOutgoingAddress !== "") {
if (tx.hasOwnProperty("destinations")) {
valid = tx.destinations.filter(destination => {
return destination.address === this.toOutgoingAddress;
}).length;
} else {
valid = false;
}
return valid;
}
if (this.toIncomingAddressIndex !== -1) {
valid = tx.hasOwnProperty("subaddr_index") && tx.subaddr_index.minor == this.toIncomingAddressIndex;
return valid;
}
return valid;
});
},
txContains(tx, value) {
// The tx can be searchable using:
// id, address, notes, amount, recipient name
const fields = [tx.txid, tx.note];
const formattedAmount = tx.amount / 1e9;
fields.push(String(formattedAmount));
// Get all addresses and names and add them on
const destinations = (tx.destinations || []).map(d => d.address);
const addresses = [tx.address, ...destinations];
const contacts = addresses
.map(this.getContact)
.filter(c => !!c)
.map(c => c.name);
fields.push(...addresses, ...contacts);
return !!fields.find(f => f.toLowerCase().includes(value.toLowerCase()));
},
getContact(address) {
return this.address_book.find(book => book.address === address);
},
pageTxList() {
this.tx_list_paged = this.tx_list_filtered.slice(0, this.limit !== -1 ? this.limit : this.page * 24 + 24);
},
loadMore: function(index, done) {
this.page = index;
if (this.limit !== -1 || this.tx_list_filtered.length < this.page * 24 + 24) {
this.$refs.scroller.stop();
}
this.pageTxList();
this.$nextTick(() => {
done();
});
},
details(tx) {
this.$refs.txDetails.tx = tx;
this.$refs.txDetails.txNotes = tx.note;
this.$refs.txDetails.isVisible = true;
},
formatHeight(tx) {
let height = tx.height;
let confirms = Math.max(0, this.wallet_height - height);
if (height == 0) return this.$t("strings.transactions.types.pending");
if (confirms < Math.max(10, tx.unlock_time - height))
return this.$t("strings.blockHeight") + `: ${height} (${confirms} confirm${confirms == 1 ? "" : "s"})`;
else return this.$t("strings.blockHeight") + `: ${height} (${this.$t("strings.transactionConfirmed")})`;
},
copyTxid(txid, event) {
event.stopPropagation();
for (let i = 0; i < event.path.length; i++) {
if (event.path[i].tagName == "BUTTON") {
event.path[i].blur();
break;
}
}
clipboard.writeText(txid);
this.$q.notify({
type: "positive",
timeout: 1000,
message: this.$t("notification.positive.copied", {
item: "Txid"
})
});
},
openExplorer(txid) {
this.$gateway.send("core", "open_explorer", {
type: "tx",
id: txid
});
}
}
};
</script>
<style lang="scss">
.tx-list {
.loki-list-item {
padding-top: 0;
padding-bottom: 0;
.loki-list-item {
padding-top: 0;
padding-bottom: 0;
}
.transaction {
.main {
margin: 0;
padding: 8px 10px;
div {
overflow: hidden;
text-overflow: ellipsis;
}
}
.transaction {
.main {
margin: 0;
padding: 8px 10px;
div {
overflow: hidden;
text-overflow: ellipsis;
}
}
.type {
div {
min-width: 100px;
margin-right: 8px;
}
}
.type {
div {
min-width: 100px;
margin-right: 8px;
}
}
}
}
</style>

View File

@ -1,54 +1,54 @@
<template>
<div class="tx-icon" v-if="type=='in'">
<div v-if="type == 'in'" class="tx-icon">
<q-icon name="call_received" size="40px" class="main-icon" />
</div>
<div class="tx-icon" v-else-if="type=='out'">
</div>
<div v-else-if="type == 'out'" class="tx-icon">
<q-icon name="call_made" size="40px" class="main-icon" />
</div>
<div class="tx-icon" v-else-if="type=='pool'">
</div>
<div v-else-if="type == 'pool'" class="tx-icon">
<q-icon name="call_received" size="40px" class="main-icon" />
<q-icon name="access_time" size="14px" class="sub-icon" />
</div>
<div class="tx-icon" v-else-if="type=='pending'">
</div>
<div v-else-if="type == 'pending'" class="tx-icon">
<q-icon name="call_made" size="40px" class="main-icon" />
<q-icon name="access_time" size="14px" class="sub-icon" />
</div>
<div class="tx-icon" v-else-if="type=='failed'">
</div>
<div v-else-if="type == 'failed'" class="tx-icon">
<q-icon name="close" size="40px" class="main-icon" color="red" />
</div>
</div>
</template>
<script>
export default {
name: "TxTypeIcon",
props: {
type: {
type: String,
required: true
},
tooltip: {
type: Boolean,
default: true,
required: false
}
name: "TxTypeIcon",
props: {
type: {
type: String,
required: true
},
}
tooltip: {
type: Boolean,
default: true,
required: false
}
}
};
</script>
<style>
.tx-icon {
width: 32px;
height: 32px;
position:relative;
width: 32px;
height: 32px;
position: relative;
}
.tx-icon .main-icon {
margin: -4px 0 0 -4px;
margin: -4px 0 0 -4px;
}
.tx-icon .sub-icon {
width: 14px;
height: 14px;
position:absolute;
bottom: -3px;
right: 0;
width: 14px;
height: 14px;
position: absolute;
bottom: -3px;
right: 0;
}
</style>

View File

@ -1,106 +1,105 @@
<template>
<div class="column wallet-info">
<div class="column wallet-info">
<div class="row justify-between items-center wallet-header loki-green">
<div class="title">{{ info.name }}</div>
<WalletSettings />
<div class="title">{{ info.name }}</div>
<WalletSettings />
</div>
<div class="wallet-content">
<div class="row justify-center">
<div class="funds column items-center">
<div class="balance">
<div class="text"><span>{{ $t("strings.lokiBalance") }}</span></div>
<div class="value"><span><FormatLoki :amount="info.balance" /></span></div>
</div>
<div class="row unlocked">
<span>{{ $t("strings.lokiUnlockedShort") }}: <FormatLoki :amount="info.unlocked_balance" /></span>
</div>
<div class="row justify-center">
<div class="funds column items-center">
<div class="balance">
<div class="text">
<span>{{ $t("strings.lokiBalance") }}</span>
</div>
<div class="value">
<span><FormatLoki :amount="info.balance"/></span>
</div>
</div>
<div class="row unlocked">
<span>{{ $t("strings.lokiUnlockedShort") }}: <FormatLoki :amount="info.unlocked_balance"/></span>
</div>
</div>
<div class="wallet-address row justify-center items-center">
<div class="address">{{ info.address }}</div>
<q-btn
color="primary" style="width:25px;"
size="sm" icon="file_copy"
ref="copy"
@click="copyAddress">
<q-tooltip anchor="center right" self="center left" :offset="[5, 10]">
{{ $t("menuItems.copyAddress") }}
</q-tooltip>
</div>
<div class="wallet-address row justify-center items-center">
<div class="address">{{ info.address }}</div>
<q-btn ref="copy" color="primary" style="width:25px;" size="sm" icon="file_copy" @click="copyAddress">
<q-tooltip anchor="center right" self="center left" :offset="[5, 10]">
{{ $t("menuItems.copyAddress") }}
</q-tooltip>
</q-btn>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
const { clipboard } = require("electron")
import { mapState } from "vuex"
import FormatLoki from "components/format_loki"
import WalletSettings from "components/wallet_settings"
const { clipboard } = require("electron");
import { mapState } from "vuex";
import FormatLoki from "components/format_loki";
import WalletSettings from "components/wallet_settings";
export default {
name: "WalletDetails",
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
info: state => state.gateway.wallet.info,
}),
methods: {
copyAddress () {
this.$refs.copy.$el.blur()
clipboard.writeText(this.info.address)
this.$q.notify({
type: "positive",
timeout: 1000,
message: this.$t("notification.positive.addressCopied")
})
},
},
components: {
FormatLoki,
WalletSettings
},
}
name: "WalletDetails",
components: {
FormatLoki,
WalletSettings
},
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
info: state => state.gateway.wallet.info
}),
methods: {
copyAddress() {
this.$refs.copy.$el.blur();
clipboard.writeText(this.info.address);
this.$q.notify({
type: "positive",
timeout: 1000,
message: this.$t("notification.positive.addressCopied")
});
}
}
};
</script>
<style lang="scss">
.wallet-info {
.wallet-header {
padding: 0.8rem 1.5rem;
.title {
font-weight: bold;
}
.wallet-header {
padding: 0.8rem 1.5rem;
.title {
font-weight: bold;
}
}
.wallet-content {
text-align: center;
background-color: #0a0a0a;
padding: 2em;
.balance {
.text {
font-size: 16px;
}
.value {
font-size: 35px;
}
}
.wallet-content {
text-align: center;
background-color: #0A0A0A;
padding: 2em;
.balance {
.text {
font-size: 16px;
}
.value {
font-size: 35px;
}
}
.wallet-address {
margin-top: 12px;
.address {
overflow: hidden;
text-overflow: ellipsis;
margin: 4px 0;
}
.q-btn {
margin-left: 8px;
}
}
.unlocked {
font-size: 14px;
font-weight: 500;
}
.wallet-address {
margin-top: 12px;
.address {
overflow: hidden;
text-overflow: ellipsis;
margin: 4px 0;
}
.q-btn {
margin-left: 8px;
}
}
.unlocked {
font-size: 14px;
font-weight: 500;
}
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -22,70 +22,70 @@
SOFTWARE.
*/
const crypto = require("crypto")
const crypto = require("crypto");
const ALGORITHM_NAME = "aes-128-gcm"
const ALGORITHM_NONCE_SIZE = 12
const ALGORITHM_TAG_SIZE = 16
const ALGORITHM_KEY_SIZE = 16
const PBKDF2_NAME = "sha256"
const PBKDF2_SALT_SIZE = 16
const PBKDF2_ITERATIONS = 32767
const ALGORITHM_NAME = "aes-128-gcm";
const ALGORITHM_NONCE_SIZE = 12;
const ALGORITHM_TAG_SIZE = 16;
const ALGORITHM_KEY_SIZE = 16;
const PBKDF2_NAME = "sha256";
const PBKDF2_SALT_SIZE = 16;
const PBKDF2_ITERATIONS = 32767;
export class SCEE {
encryptString (plaintext, password) {
// Generate a 128-bit salt using a CSPRNG.
let salt = crypto.randomBytes(PBKDF2_SALT_SIZE)
encryptString(plaintext, password) {
// Generate a 128-bit salt using a CSPRNG.
let salt = crypto.randomBytes(PBKDF2_SALT_SIZE);
// Derive a key using PBKDF2.
let key = crypto.pbkdf2Sync(new Buffer(password, "utf8"), salt, PBKDF2_ITERATIONS, ALGORITHM_KEY_SIZE, PBKDF2_NAME)
// Derive a key using PBKDF2.
let key = crypto.pbkdf2Sync(new Buffer(password, "utf8"), salt, PBKDF2_ITERATIONS, ALGORITHM_KEY_SIZE, PBKDF2_NAME);
// Encrypt and prepend salt.
let ciphertextAndNonceAndSalt = Buffer.concat([ salt, this.encrypt(new Buffer(plaintext, "utf8"), key) ])
// Encrypt and prepend salt.
let ciphertextAndNonceAndSalt = Buffer.concat([salt, this.encrypt(new Buffer(plaintext, "utf8"), key)]);
// Return as base64 string.
return ciphertextAndNonceAndSalt.toString("base64")
}
// Return as base64 string.
return ciphertextAndNonceAndSalt.toString("base64");
}
decryptString (base64CiphertextAndNonceAndSalt, password) {
// Decode the base64.
let ciphertextAndNonceAndSalt = new Buffer(base64CiphertextAndNonceAndSalt, "base64")
decryptString(base64CiphertextAndNonceAndSalt, password) {
// Decode the base64.
let ciphertextAndNonceAndSalt = new Buffer(base64CiphertextAndNonceAndSalt, "base64");
// Create buffers of salt and ciphertextAndNonce.
let salt = ciphertextAndNonceAndSalt.slice(0, PBKDF2_SALT_SIZE)
let ciphertextAndNonce = ciphertextAndNonceAndSalt.slice(PBKDF2_SALT_SIZE)
// Create buffers of salt and ciphertextAndNonce.
let salt = ciphertextAndNonceAndSalt.slice(0, PBKDF2_SALT_SIZE);
let ciphertextAndNonce = ciphertextAndNonceAndSalt.slice(PBKDF2_SALT_SIZE);
// Derive the key using PBKDF2.
let key = crypto.pbkdf2Sync(new Buffer(password, "utf8"), salt, PBKDF2_ITERATIONS, ALGORITHM_KEY_SIZE, PBKDF2_NAME)
// Derive the key using PBKDF2.
let key = crypto.pbkdf2Sync(new Buffer(password, "utf8"), salt, PBKDF2_ITERATIONS, ALGORITHM_KEY_SIZE, PBKDF2_NAME);
// Decrypt and return result.
return this.decrypt(ciphertextAndNonce, key).toString("utf8")
}
// Decrypt and return result.
return this.decrypt(ciphertextAndNonce, key).toString("utf8");
}
encrypt (plaintext, key) {
// Generate a 96-bit nonce using a CSPRNG.
let nonce = crypto.randomBytes(ALGORITHM_NONCE_SIZE)
encrypt(plaintext, key) {
// Generate a 96-bit nonce using a CSPRNG.
let nonce = crypto.randomBytes(ALGORITHM_NONCE_SIZE);
// Create the cipher instance.
let cipher = crypto.createCipheriv(ALGORITHM_NAME, key, nonce)
// Create the cipher instance.
let cipher = crypto.createCipheriv(ALGORITHM_NAME, key, nonce);
// Encrypt and prepend nonce.
let ciphertext = Buffer.concat([ cipher.update(plaintext), cipher.final() ])
// Encrypt and prepend nonce.
let ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
return Buffer.concat([ nonce, ciphertext, cipher.getAuthTag() ])
}
return Buffer.concat([nonce, ciphertext, cipher.getAuthTag()]);
}
decrypt (ciphertextAndNonce, key) {
// Create buffers of nonce, ciphertext and tag.
let nonce = ciphertextAndNonce.slice(0, ALGORITHM_NONCE_SIZE)
let ciphertext = ciphertextAndNonce.slice(ALGORITHM_NONCE_SIZE, ciphertextAndNonce.length - ALGORITHM_TAG_SIZE)
let tag = ciphertextAndNonce.slice(ciphertext.length + ALGORITHM_NONCE_SIZE)
decrypt(ciphertextAndNonce, key) {
// Create buffers of nonce, ciphertext and tag.
let nonce = ciphertextAndNonce.slice(0, ALGORITHM_NONCE_SIZE);
let ciphertext = ciphertextAndNonce.slice(ALGORITHM_NONCE_SIZE, ciphertextAndNonce.length - ALGORITHM_TAG_SIZE);
let tag = ciphertextAndNonce.slice(ciphertext.length + ALGORITHM_NONCE_SIZE);
// Create the cipher instance.
let cipher = crypto.createDecipheriv(ALGORITHM_NAME, key, nonce)
// Create the cipher instance.
let cipher = crypto.createDecipheriv(ALGORITHM_NAME, key, nonce);
// Decrypt and return result.
cipher.setAuthTag(tag)
return Buffer.concat([ cipher.update(ciphertext), cipher.final() ])
}
// Decrypt and return result.
cipher.setAuthTag(tag);
return Buffer.concat([cipher.update(ciphertext), cipher.final()]);
}
}

View File

@ -1,244 +1,260 @@
import { ipcRenderer } from "electron"
import { Notify, Dialog, Loading, LocalStorage } from "quasar"
import { EventEmitter } from "events"
import { SCEE } from "./SCEE-Node"
import { i18n, changeLanguage } from "src/plugins/i18n"
import { ipcRenderer } from "electron";
import { Notify, Dialog, Loading, LocalStorage } from "quasar";
import { EventEmitter } from "events";
import { SCEE } from "./SCEE-Node";
import { i18n, changeLanguage } from "src/plugins/i18n";
export class Gateway extends EventEmitter {
constructor (app, router) {
super()
this.app = app
this.router = router
this.token = null
this.scee = new SCEE()
constructor(app, router) {
super();
this.app = app;
this.router = router;
this.token = null;
this.scee = new SCEE();
// Set the initial language
let language = LocalStorage.has("language") ? LocalStorage.get.item("language") : "en-us"
this.setLanguage(language)
// Set the initial language
let language = LocalStorage.has("language") ? LocalStorage.get.item("language") : "en-us";
this.setLanguage(language);
let theme = LocalStorage.has("theme") ? LocalStorage.get.item("theme") : "dark"
this.app.store.commit("gateway/set_app_data", {
config: {
appearance: {
theme
}
}
})
this.app.store.watch(state => state.gateway.app.config.appearance.theme, (theme) => {
LocalStorage.set("theme", theme)
})
let theme = LocalStorage.has("theme") ? LocalStorage.get.item("theme") : "dark";
this.app.store.commit("gateway/set_app_data", {
config: {
appearance: {
theme
}
}
});
this.app.store.watch(
state => state.gateway.app.config.appearance.theme,
theme => {
LocalStorage.set("theme", theme);
}
);
this.closeDialog = false
this.closeDialog = false;
this.app.store.commit("gateway/set_app_data", {
status: {
code: 1 // Connecting to backend
}
})
this.app.store.commit("gateway/set_app_data", {
status: {
code: 1 // Connecting to backend
}
});
ipcRenderer.on("initialize", (event, data) => {
this.token = data.token
setTimeout(() => {
this.ws = new WebSocket("ws://127.0.0.1:" + data.port)
this.ws.addEventListener("open", () => { this.open() })
this.ws.addEventListener("message", (e) => { this.receive(e.data) })
}, 1000)
})
ipcRenderer.on("initialize", (event, data) => {
this.token = data.token;
setTimeout(() => {
this.ws = new WebSocket("ws://127.0.0.1:" + data.port);
this.ws.addEventListener("open", () => {
this.open();
});
this.ws.addEventListener("message", e => {
this.receive(e.data);
});
}, 1000);
});
ipcRenderer.on("confirmClose", () => {
this.confirmClose(i18n.t("dialog.exit.message"))
})
ipcRenderer.on("confirmClose", () => {
this.confirmClose(i18n.t("dialog.exit.message"));
});
ipcRenderer.on("showQuitScreen", () => {
if (this.router) {
this.router.replace({ path: "/quit" })
}
})
ipcRenderer.on("showQuitScreen", () => {
if (this.router) {
this.router.replace({ path: "/quit" });
}
});
}
open() {
this.app.store.commit("gateway/set_app_data", {
status: {
code: 2 // Loading config
}
});
this.send("core", "init");
}
confirmClose(msg, restart = false) {
if (this.closeDialog) {
return;
}
this.closeDialog = true;
const key = restart ? "restart" : "exit";
Dialog.create({
title: i18n.t(`dialog.${key}.title`),
message: msg,
ok: {
label: i18n.t(`dialog.${key}.ok`)
},
cancel: {
flat: true,
label: i18n.t("dialog.buttons.cancel"),
color: this.app.store.state.gateway.app.config.appearance.theme === "dark" ? "white" : "dark"
}
})
.then(() => {
this.closeDialog = false;
Loading.hide();
this.router.replace({ path: "/quit" });
ipcRenderer.send("confirmClose", restart);
})
.catch(() => {
this.closeDialog = false;
});
}
send(module, method, data = {}) {
let message = {
module,
method,
data
};
let encrypted_data = this.scee.encryptString(JSON.stringify(message), this.token);
this.ws.send(encrypted_data);
}
geti18n(key) {
return Array.isArray(key) ? i18n.t(...key) : i18n.t(key);
}
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));
if (!decrypted_data || !decrypted_data.event || !decrypted_data.data) {
return;
}
open () {
this.app.store.commit("gateway/set_app_data", {
status: {
code: 2 // Loading config
}
})
this.send("core", "init")
switch (decrypted_data.event) {
case "set_language": {
const { lang } = decrypted_data.data;
this.setLanguage(lang);
break;
}
case "set_has_password":
this.emit("has_password", decrypted_data.data);
break;
case "set_valid_address":
this.emit("validate_address", decrypted_data.data);
break;
case "set_app_data":
this.app.store.commit("gateway/set_app_data", decrypted_data.data);
break;
case "set_daemon_data":
this.app.store.commit("gateway/set_daemon_data", decrypted_data.data);
break;
case "set_wallet_data":
case "set_wallet_error":
this.app.store.commit("gateway/set_wallet_data", decrypted_data.data);
break;
case "reset_wallet_error":
this.app.store.dispatch("gateway/resetWalletStatus");
break;
case "set_tx_status": {
const data = { ...decrypted_data.data };
if (data.i18n) {
data.message = this.geti18n(data.i18n);
}
this.app.store.commit("gateway/set_tx_status", data);
break;
}
case "set_snode_status": {
const data = { ...decrypted_data.data };
// We have multiple nested objects in service_node_status
for (const key in data) {
if (data[key].i18n) {
data[key].message = this.geti18n(data[key].i18n);
}
}
this.app.store.commit("gateway/set_snode_status", data);
break;
}
case "set_prove_transaction_status": {
const data = { ...decrypted_data.data };
if (data.i18n) {
data.message = this.geti18n(data.i18n);
}
this.app.store.commit("gateway/set_prove_transaction_status", data);
break;
}
case "set_check_transaction_status": {
const data = { ...decrypted_data.data };
if (data.i18n) {
data.message = this.geti18n(data.i18n);
}
this.app.store.commit("gateway/set_check_transaction_status", data);
break;
}
case "set_old_gui_import_status":
this.app.store.commit("gateway/set_old_gui_import_status", decrypted_data.data);
break;
case "wallet_list":
this.app.store.commit("gateway/set_wallet_list", decrypted_data.data);
break;
case "settings_changed_reboot":
this.confirmClose(i18n.t("dialog.restart.message"), true);
break;
case "show_notification": {
let notification = {
type: "positive",
timeout: 1000,
message: ""
};
const { data } = decrypted_data;
if (data.i18n) {
notification.message = this.geti18n(data.i18n);
}
Notify.create(Object.assign(notification, data));
break;
}
case "show_loading":
Loading.show({ ...(decrypted_data.data || {}) });
break;
case "hide_loading":
Loading.hide();
break;
case "return_to_wallet_select":
this.router.replace({ path: "/wallet-select" });
setTimeout(() => {
// short delay to prevent wallet data reaching the
// websocket moments after we close and reset data
this.app.store.dispatch("gateway/resetWalletData");
}, 250);
break;
}
}
confirmClose (msg, restart = false) {
if (this.closeDialog) {
return
}
this.closeDialog = true
const key = restart ? "restart" : "exit"
Dialog.create({
title: i18n.t(`dialog.${key}.title`),
message: msg,
ok: {
label: i18n.t(`dialog.${key}.ok`)
},
cancel: {
flat: true,
label: i18n.t("dialog.buttons.cancel"),
color: this.app.store.state.gateway.app.config.appearance.theme == "dark" ? "white" : "dark"
}
}).then(() => {
this.closeDialog = false
Loading.hide()
this.router.replace({ path: "/quit" })
ipcRenderer.send("confirmClose", restart)
}).catch(() => {
this.closeDialog = false
})
}
send (module, method, data = {}) {
let message = {
module,
method,
data
}
let encrypted_data = this.scee.encryptString(JSON.stringify(message), this.token)
this.ws.send(encrypted_data)
}
geti18n (key) {
return Array.isArray(key) ? i18n.t(...key) : i18n.t(key)
}
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))
if (typeof decrypted_data !== "object" ||
!decrypted_data.hasOwnProperty("event") ||
!decrypted_data.hasOwnProperty("data")) { return }
switch (decrypted_data.event) {
case "set_language":
const { lang } = decrypted_data.data
this.setLanguage(lang)
break
case "set_has_password":
this.emit("has_password", decrypted_data.data)
break
case "set_valid_address":
this.emit("validate_address", decrypted_data.data)
break
case "set_app_data":
this.app.store.commit("gateway/set_app_data", decrypted_data.data)
break
case "set_daemon_data":
this.app.store.commit("gateway/set_daemon_data", decrypted_data.data)
break
case "set_wallet_data":
case "set_wallet_error":
this.app.store.commit("gateway/set_wallet_data", decrypted_data.data)
break
case "reset_wallet_error":
this.app.store.dispatch("gateway/resetWalletStatus")
break
case "set_tx_status": {
const data = { ...decrypted_data.data }
if (data.i18n) {
data.message = this.geti18n(data.i18n)
}
this.app.store.commit("gateway/set_tx_status", data)
break
}
case "set_snode_status": {
const data = { ...decrypted_data.data }
// We have multiple nested objects in service_node_status
for (const key in data) {
if (data[key].i18n) { data[key].message = this.geti18n(data[key].i18n) }
}
this.app.store.commit("gateway/set_snode_status", data)
break
}
case "set_prove_transaction_status": {
const data = { ...decrypted_data.data }
if (data.i18n) {
data.message = this.geti18n(data.i18n)
}
this.app.store.commit("gateway/set_prove_transaction_status", data)
break
}
case "set_check_transaction_status": {
const data = { ...decrypted_data.data }
if (data.i18n) {
data.message = this.geti18n(data.i18n)
}
this.app.store.commit("gateway/set_check_transaction_status", data)
break
}
case "set_old_gui_import_status":
this.app.store.commit("gateway/set_old_gui_import_status", decrypted_data.data)
break
case "wallet_list":
this.app.store.commit("gateway/set_wallet_list", decrypted_data.data)
break
case "settings_changed_reboot":
this.confirmClose(i18n.t("dialog.restart.message"), true)
break
case "show_notification": {
let notification = {
type: "positive",
timeout: 1000,
message: ""
}
const { data } = decrypted_data
if (data.i18n) {
notification.message = this.geti18n(data.i18n)
}
Notify.create(Object.assign(notification, data))
break
}
case "show_loading":
Loading.show({ ...(decrypted_data.data || {}) })
break
case "hide_loading":
Loading.hide()
break
case "return_to_wallet_select":
this.router.replace({ path: "/wallet-select" })
setTimeout(() => {
// short delay to prevent wallet data reaching the
// websocket moments after we close and reset data
this.app.store.dispatch("gateway/resetWalletData")
}, 250)
break
}
}
setLanguage (lang) {
changeLanguage(lang).then(() => {
LocalStorage.set("language", lang)
}).catch(() => {
Notify.create({
type: "negative",
timeout: 2000,
message: i18n.t("notification.errors.failedToSetLanguage", { lang })
})
})
}
setLanguage(lang) {
changeLanguage(lang)
.then(() => {
LocalStorage.set("language", lang);
})
.catch(() => {
Notify.create({
type: "negative",
timeout: 2000,
message: i18n.t("notification.errors.failedToSetLanguage", {
lang
})
});
});
}
}

View File

@ -1,489 +1,501 @@
export default {
buttons: {
// All button text is uppercased in the gui
advanced: "ADVANCED",
all: "ALLES",
back: "ZURÜCK",
browse: "DURCHSUCHEN",
cancel: "ABBRECHEN",
change: "ÄNDERN",
check: "CHECK",
clear: "CLEAR",
close: "SCHLIESSEN",
contacts: "KONTAKTE",
copyAddress: "ADRESSE KOPIEREN",
copySignature: "COPY SIGNATURE",
createWallet: "WALLET ERSTELLEN",
delete: "LÖSCHEN",
edit: "BEARBEITEN",
export: "EXPORTIEREN",
generate: "GENERATE",
import: "IMPORTIEREN",
importWallet: "WALLET IMPORTIEREN | WALLETS IMPORTIEREN",
next: "WEITER",
openWallet: "WALLET ÖFFNEN",
receive: "EMPFANGEN",
registerServiceNode: "SERVICE NODE REGISTRIEREN",
rescan: "ERNEUT SCANNEN",
restoreWallet: "WALLET WIEDERHERSTELLEN",
save: "SPEICHERN",
saveTxNotes: "TX NOTES SPEICHERN",
selectLocation: "STANDORT AUSWÄHLEN",
selectWalletFile: "WALLET DATEU AUSWÄHLEN",
send: "SENDEN",
sendCoins: "COINS SENDEN",
serviceNode: "SERVICE NODE",
settings: "EINSTELLUNGEN",
showQRCode: "QR CODE ANZEIGEN",
showTxDetails: "TX DETAILS ANZEIGEN",
stake: "STAKE",
sweepAll: "ALLES BEREINIGEN",
unlock: "UNLOCK",
viewOnExplorer: "IN EXPLORER ANZEIGEN"
},
dialog: {
// Generic buttons
buttons: {
// All button text is uppercased in the gui
advanced: "ADVANCED",
all: "ALLES",
back: "ZURÜCK",
browse: "DURCHSUCHEN",
cancel: "ABBRECHEN",
change: "ÄNDERN",
check: "CHECK",
clear: "CLEAR",
close: "SCHLIESSEN",
contacts: "KONTAKTE",
copyAddress: "ADRESSE KOPIEREN",
copySignature: "COPY SIGNATURE",
createWallet: "WALLET ERSTELLEN",
delete: "LÖSCHEN",
edit: "BEARBEITEN",
export: "EXPORTIEREN",
generate: "GENERATE",
import: "IMPORTIEREN",
importWallet: "WALLET IMPORTIEREN | WALLETS IMPORTIEREN",
next: "WEITER",
openWallet: "WALLET ÖFFNEN",
receive: "EMPFANGEN",
registerServiceNode: "SERVICE NODE REGISTRIEREN",
rescan: "ERNEUT SCANNEN",
restoreWallet: "WALLET WIEDERHERSTELLEN",
save: "SPEICHERN",
saveTxNotes: "TX NOTES SPEICHERN",
selectLocation: "STANDORT AUSWÄHLEN",
selectWalletFile: "WALLET DATEU AUSWÄHLEN",
send: "SENDEN",
sendCoins: "COINS SENDEN",
serviceNode: "SERVICE NODE",
settings: "EINSTELLUNGEN",
showQRCode: "QR CODE ANZEIGEN",
showTxDetails: "TX DETAILS ANZEIGEN",
stake: "STAKE",
sweepAll: "ALLES BEREINIGEN",
unlock: "UNLOCK",
viewOnExplorer: "IN EXPLORER ANZEIGEN"
ok: "OK",
cancel: "ABBRECHEN",
open: "ÖFFNEN"
},
dialog: {
// Generic buttons
buttons: {
ok: "OK",
cancel: "ABBRECHEN",
open: "ÖFFNEN"
},
// Dialogs
banPeer: {
title: "Peer blockieren",
peerDetailsTitle: "Peer Details",
message: "Dauer der Blockierung des Peers eingeben.\nStandard 3600 = 1 Stunde.",
ok: "Peer blockieren"
},
copyAddress: {
title: "Adresse kopieren",
message: "Es is eine Payment ID mit dieser Adresse verbunden.\n Bitte die Paymend ID separat kopieren"
},
copyPrivateKeys: {
// Copy {seedWords/viewKey/spendKey}
title: "Kopieren {type}",
message: "Sei vorsichtig, wem du deine Private Keys sendest, denn derjenige erhält dadurch die Kontrolle über deine Einlagen",
seedWords: "Seed Wörter",
viewKey: "View Key",
spendKey: "Spend Key"
},
deleteWallet: {
title: "Wallet löschen",
message: "Bist du dir absolut sicher, dass du deine Wallet löschen willst?\n Bitte stelle sicher, dass du deinen Private Key gesichert hast.\n DIESER SCHRITT KANN NICHT RÜCKGÄNGIG GEMACHT WERDEN!",
ok: "LÖSCHEN"
},
exit: {
title: "Beenden",
message: "Bist du sicher, dass du das Programm beenden möchtest?",
ok: "Beenden"
},
keyImages: {
title: "{type} key images",
message: "Möchtest du key images {type}",
export: "Exportieren",
import: "Importieren"
},
noPassword: {
title: "Kein Passwort angelegt",
message: "Bist du sicher, dass du eine Wallet ohne Passwort erstellen möchtest?",
ok: "JA"
},
password: {
title: "Passwort",
message: "Wallet Passwort eingeben um fortzufahren"
},
registerServiceNode: {
title: "Service Node registrieren",
message: "Möchtest du einen Service Node registrieren?",
ok: "REGISTRIEREN "
},
rescan: {
title: "Wallet erneut scannen",
message: "Warnung: Einige Informationen über vorherige Transaktionen\nsowie Adressen von Empfängern gehen verloren",
ok: "ERNEUT SCANNEN"
},
restart: {
title: "Erneut starten",
message: "Änderungen erfordern einen Neustart. Möchtest du jetzt neu starten?",
ok: "NEUSTART"
},
showPrivateKeys: {
title: "Private Keys Anzeigen",
message: "Möchtest du deinen Private Key anzeigen?",
ok: "ANZEIGEN"
},
stake: {
title: "Stake",
message: "Möchtest du staken?",
ok: "Stake"
},
sweepAll: {
title: "Alles Bereinigen",
message: "Möchtest du alles bereinigen?",
ok: "ALLES BEREINIGEN"
},
sweepAllWarning: {
title: "Sweep all warning",
message: "You are about to combine all of your unspent funds by sending a transaction to yourself, your wallet may show a balance of 0 temporarily, after 10 blocks your funds will unlock and you may stake normally.",
ok: "CONTINUE"
},
switchWallet: {
title: "Wallet wechseln",
closeMessage: "Bist du sicher, dass du die aktuelle Wallet schliessen möchtest?",
restartMessage: "Die Wallet RPC synchronisiert sich gerade\n Wenn du deine Wallet wechseln möchtest, musst du die Anwendung erneut starten. \n Die Synchronisation wird abgebrochen und du musst die Blockchain erneut scannen. "
},
transactionDetails: {
title: "Transaktionsdetails",
ok: "SCHLIESSEN"
},
transfer: {
title: "Transferieren",
message: "Möchtest du die Transaktion senden?",
ok: "SENDEN"
},
unlockConfirm: {
title: "Unlock bestätigen",
ok: "UNLOCK"
},
unlockServiceNode: {
title: "Unlock Service Node",
confirmTitle: "Unlock bestätigen",
message: "Möchtest du den Service Node „unlocken“?",
ok: "UNLOCK"
},
unlockServiceNodeWarning: {
title: "Unlock service node warning",
message: "Unlocking a partial stake in a node will also unstake for any other participants, if staking in a shared node its best to let the operator and other participants know you are unstaking.",
ok: "CONTINUE"
}
// Dialogs
banPeer: {
title: "Peer blockieren",
peerDetailsTitle: "Peer Details",
message: "Dauer der Blockierung des Peers eingeben.\nStandard 3600 = 1 Stunde.",
ok: "Peer blockieren"
},
fieldLabels: {
// Field labels are also all uppercased
address: "ADRESSE",
amount: "BETRAG",
awardRecepientAddress: "EMPFÄNGER ADRESSE FÜR DIE VERGÜTUNG",
confirmPassword: "PASSWORT BESTÄTIGEN",
chooseNetwork: "NETZWERK AUSWÄHLEN",
daemonLogLevel: "DAEMON LOG LEVEL",
daemonP2pPort: "DAEMON P2P PORT",
daemonZMQPort: "DAEMON ZMQ PORT",
dataStoragePath: "DATENSICHERUNGSPFAD",
filter: "FILTER",
filterTransactionType: "FILTERN NACH TRANSAKTIONSTYP",
internalWalletPort: "INTERNER WALLET PORT",
keyImages: {
exportDirectory: "KEY IMAGE EXPORTVERZEICHNIS",
importFile: "KEY IMAGE IMPORT FILE"
},
limitDownloadRate: "DOWNLOAD RATE LIMITIEREN",
limitUploadRate: "UPLOAD RATE LIMITIEREN",
localDaemonIP: "LOKALE DAEMON IP",
localDaemonPort: "LOKALER DAEMON PORT",
maxIncomingPeers: "MAX EINGEHENDE PEERS",
maxOutgoingPeers: "MAX AUSGEHENDE PEERS",
message: "MESSAGE",
mnemonicSeed: "MNEMONIC SEED",
name: "NAME",
newWalletName: "NEUER WALLET NAME",
network: "NETZWERK",
notes: "NOTIZEN",
optional: "OPTIONAL",
password: "PASSWORT",
paymentId: "PAYMENT ID",
priority: "PRIORITÄT",
remoteNodeHost: "REMOTE NODE HOST",
remoteNodePort: "REMOTE NODE PORT",
restoreFromBlockHeight: "WIEDERHERSTELLUNG VON BLOCKHÖHE",
restoreFromDate: "WIEDERHERSTELLUNG VON DATUM",
seedLanguage: "SEED SPRACHE",
serviceNodeCommand: "SERVICE NODE COMMAND",
serviceNodeKey: "SERVICE NODE KEY",
signature: "SIGNATURE",
transactionId: "TRANSACTION ID",
walletFile: "WALLET FILE",
walletLogLevel: "WALLET LOG LEVEL",
walletName: "WALLET NAME",
walletRPCPort: "WALLET RPC PORT",
walletStoragePath: "WALLET SICHERUNGSPFAD",
// These are specific labels which do not get uppercased
confirmNewPassword: "Neues Passwort bestätigen",
newPassword: "Neues Passwort",
oldPassword: "Altes Passwort",
rescanFullBlockchain: "Gesamte Blockchain erneut scannen",
rescanSpentOutputs: "Spent Outputs erneut scannen",
transactionNotes: "Transaktionsnotizen"
copyAddress: {
title: "Adresse kopieren",
message: "Es is eine Payment ID mit dieser Adresse verbunden.\n Bitte die Paymend ID separat kopieren"
},
footer: {
ready: "FERTIG",
remote: "REMOTE",
scanning: "SCANNEN",
status: "Status",
syncing: "Synchronisierung",
wallet: "Wallet"
copyPrivateKeys: {
// Copy {seedWords/viewKey/spendKey}
title: "Kopieren {type}",
message:
"Sei vorsichtig, wem du deine Private Keys sendest, denn derjenige erhält dadurch die Kontrolle über deine Einlagen",
seedWords: "Seed Wörter",
viewKey: "View Key",
spendKey: "Spend Key"
},
menuItems: {
about: "Über ",
changePassword: "Passwort ändern",
copyAddress: "Adresse kopieren",
copyQR: "QR Code kopieren",
copySeedWords: "Seed Wörter kopieren",
copySpendKey: "Spend Key kopieren",
copyServiceNodeKey: "Copy service node key",
copyTransactionId: "Transaktions ID kopieren",
copyViewKey: "View Key kopieren",
createNewWallet: "Neue Wallet erstellen",
deleteWallet: "Wallet löschen",
exit: "Loki GUI Wallet schliessen",
importOldGUIWallet: "Wallets von alter GUI importieren",
manageKeyImages: "Key Images verwalten",
openWallet: "Wallet öffnen",
rescanWallet: "Wallet erneut scannen",
restoreWalletFile: "Wallet aus Datei wiederherstellen",
restoreWalletSeed: "Wallet aus Seed wiederherstellen ",
saveQR: "QR in Datei speichern",
sendToThisAddress: "Zu dieser Adresse senden",
settings: "Einstellungen",
showDetails: "Details anzeigen",
showPrivateKeys: "Zeige Private Keys",
showQRCode: "Zeige QR Code",
switchWallet: "Wallet wechseln",
viewOnExplorer: "Zeige in Explorer"
deleteWallet: {
title: "Wallet löschen",
message:
"Bist du dir absolut sicher, dass du deine Wallet löschen willst?\n Bitte stelle sicher, dass du deinen Private Key gesichert hast.\n DIESER SCHRITT KANN NICHT RÜCKGÄNGIG GEMACHT WERDEN!",
ok: "LÖSCHEN"
},
notification: {
positive: {
addressCopied: "Adresse in die Zwischenablage kopiert",
bannedPeer: "Blockiert {host} bis {time}",
copied: "{item} in Zwischenablage kopiert",
itemSaved: "{item} gespeichert nach {filename}",
keyImages: {
exported: "Key images exportiert nach {filename}",
imported: "Key images importiert"
},
passwordUpdated: "Passwort aktualisiert",
qrCopied: "QR Code in die Zwischenablage kopiert",
registerServiceNodeSuccess: "Service Node erfolgreich registriert ",
sendSuccess: "Transaktion erfolgreich gesendet",
signatureCopied: "Signature copied to clipboard",
stakeSuccess: "Staking erfolgreich",
transactionNotesSaved: "Notizen zur Transaktion gesichert"
},
errors: {
banningPeer: "Fehler bei der Blockierung des Peer",
cannotAccessRemoteNode: "Remote Node nicht erreichbar, bitte versuche es mit einer anderen Remote Node",
changingPassword: "Fehler beim Ändern des Passworts",
copyWalletFail: "Fehler beim Kopieren der Wallet",
copyingPrivateKeys: "Fehler beim Kopieren der Private Keys",
dataPathNotFound: "Pfad zur Speicherung nicht gefunden",
differentNetType: "Remote Node benutzt einen anderen „nettype“",
enterSeedWords: "Seed Wörter eingeben",
enterTransactionId: "Enter transaction ID",
enterTransactionProof: "Enter transaction proof",
enterWalletName: "Wallet Namen eingeben",
errorSavingItem: "Fehler beim Speichern {item}",
failedServiceNodeUnlock: "Fehler beim Service Node unlock",
failedToSetLanguage: "Fehler bei der Auswahl der Sprache: {lang}",
failedWalletImport: "Fehler beim Importieren der Wallet",
failedWalletOpen: "Fehler beim Öffnen der Wallet. Bitte versuche es erneut",
internalError: "Interner Fehler",
invalidAddress: "Adresse nicht gültig",
invalidAmount: "Betrag nicht gültig",
invalidOldPassword: "Ungültiges altes Passwort",
invalidPassword: "Ungültiges Passwort",
invalidPaymentId: "Payment ID nicht gültig",
invalidPrivateViewKey: "Ungültiger Private View Key",
invalidPublicAddress: "Ungültige öffentliche Adresse",
invalidRestoreDate: "Ungültiges Wiederherstellungsdatum",
invalidRestoreHeight: "Ungültige Wiederherstellungshöhe",
invalidSeedLength: "Ungültige Seed Wortlänge",
invalidServiceNodeCommand: "Bitte füge den Service Node Registrierungsbefehl ein",
invalidServiceNodeKey: "Service Node Key nicht gültig",
invalidWalletPath: "Ungültiger Wallet Pfad",
keyImages: {
exporting: "Fehler beim Export der Key images",
reading: "Fehler beim lesen der Key images",
importing: "Fehler beim Import der Key Images"
},
negativeAmount: "Betrag kann nicht negativ sein ",
newPasswordNoMatch: "Neue Passwörter stimmen nicht überein",
newPasswordSame: "Neues Passwort darf nicht identisch sein",
notEnoughBalance: "Nicht genug frei verfügbares Guthaben",
passwordNoMatch: "Passwörter stimmen nicht überein",
remoteCannotBeReached: "Remote daemon ist nicht erreichbar",
selectWalletFile: "Select a wallet file",
unknownError: "Ein unbekannter Fehler ist aufgetreten ",
walletAlreadyExists: "Wallet mit diesem Namen existiert bereits",
walletPathNotFound: "Wallet Daten Pfad nicht gefunden",
zeroAmount: "Betrag muss grösser als null sein"
},
warnings: {
noKeyImageExport: "Keine Key Images zum Exportieren gefunden",
usingLocalNode: "Zugang zur Remote Node nicht möglich, wechsle zur lokalen Node",
usingRemoteNode: "llokid nicht gefunden, benutze eine Remote Node"
}
exit: {
title: "Beenden",
message: "Bist du sicher, dass du das Programm beenden möchtest?",
ok: "Beenden"
},
placeholders: {
additionalNotes: "Zusätzliche Notizen",
addressBookName: "Zugehörige Namen zu dieser Adresse",
filterTx: "Enter an ID, name, address or amount",
hexCharacters: "{count} Hexadezimal Zeichen",
mnemonicSeed: "25 (oder 24) mnemonic Seed Wörter",
pasteTransactionId: "Paste transaction ID",
pasteTransactionProof: "Paste transaction proof",
proveOptionalMessage: "Optional message against which the signature is signed",
recipientWalletAddress: "Recipient's wallet address",
selectAFile: "Bitte Datei auswählen",
transactionNotes: "Zusätzliche Notizen die an die Transaktions gehängt werden sollen",
walletName: "Ein Name für deine Wallet",
walletPassword: "Ein optionales Passwort für die Wallet"
keyImages: {
title: "{type} key images",
message: "Möchtest du key images {type}",
export: "Exportieren",
import: "Importieren"
},
strings: {
addAddressBookEntry: "Adressbuch Eintrag hinzufügen",
addressBookDetails: "Adressbuch details",
addressBookIsEmpty: "Adressbuch ist leer",
addresses: {
myPrimaryAddress: "Meine primäre Adresse",
myUnusedAddresses: "Meine ungenutzten Adressen",
myUsedAddresses: "Meine benutzen Adressen",
primaryAddress: "Primäre Adresse",
subAddress: "Sub-Adresse",
subAddressIndex: "Index {index}"
},
advancedOptions: "Erweiterte Optionen",
bannedPeers: {
title: "Blockierte Peers (Blockierungen werden entfernt, wenn Wallet neu gestartet wird)",
bannedUntil: "Blockieren bis {time}"
},
blockHeight: "Höhe",
checkTransaction: {
description: "Verify that funds were paid to an address by supplying the transaction ID, the recipient address, the message used for signing and the signature.\nFor a 'Spend Proof' you dont need to provide the recipient address.",
infoTitles: {
confirmations: "Confirmations",
inPool: "In pool",
validTransaction: "Valid transaction",
received: "Received amount"
},
validTransaction: {
no: "NO",
yes: "YES"
}
},
closing: "schliessen",
connectingToBackend: "Verbinden zum Backend",
contribution: "Contribution",
daemon: {
local: {
title: "Nur lokaler Daemon",
description: "Volle Sicherheit. Wallet wird die gesamte Blockchain herunterladen. Du kannst keine Transaktionen durchführen, solange die Synchronisation nicht vollständig beendet wurde"
},
localRemote: {
title: "Lokal + Remote Daemon",
description: "Schnell starten mit dieser Standard Option. Wallet wird die Blockchain vollständig herunterladen, aber während der Synchronisation eine Remote Node nutzen"
},
remote: {
title: "Nur Remote Node",
description: "Etwas weniger sicher. Wallet verbindet sich mit einer Remote Node, um Transaktionen über diese durchzuführen"
}
},
destinationUnknown: "Ziel unbekannt",
editAddressBookEntry: "Adressbucheintrag bearbeiten",
loadingSettings: "Einstellungen werden geladen",
lokiBalance: "Guthaben",
lokiUnlockedBalance: "frei verfügbares Guthaben",
lokiUnlockedShort: "frei verfügbar",
noTransactionsFound: "Keine Transaktionen gefunden",
notes: "Notizen",
numberOfUnspentOutputs: "Anzahl der unspent outputs",
paymentID: "Payment ID",
peerList: "Peer Liste",
priorityOptions: {
automatic: "Automatisch",
fast: "Schnell",
fastest: "Am schnellsten",
normal: "Normal",
slow: "Langsam"
},
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: "Lese Wallet Liste",
recentIncomingTransactionsToAddress: "Kürzlich eingegangene Transaktionen zu dieser Adresse",
recentTransactionsWithAddress: "Kürzlich durchgeführte Transaktionen mit dieser Adresse",
rescanModalDescription: "Auswahl gesamter Rescan oder nur spent outputs",
saveSeedWarning: "Bitte kopiere und verwahre deinen \"Seed\" an einem sicheren Ort",
saveToAddressBook: "In Adressbuch speichern",
seedWords: "Seed Wörter",
selectLanguage: "Sprache auswählen",
serviceNodeRegistrationDescription: "Hier den {registerComand} Befehl, der mit Hilfe des Daemons durch das Kommando {prepareCommand} erzeugt wurde, eingeben, um eine Service Node zu aktivieren",
spendKey: "Spend key",
startingDaemon: "Daemon wird gestartet",
startingWallet: "Wallet wird gestartet",
switchToDateSelect: "Wechsel zur Selektion nach Datum",
switchToHeightSelect: "Wechsel zu Selektion nach Höhe",
transactionID: "Transaktions ID",
transactionConfirmed: "Transaktion bestätigt",
transactions: {
amount: "Betrag",
description: "{type} Transaktion",
fee: "Gebühr",
paidBySender: "Vom Absender bezahlt",
received: "Empfangen",
sent: "Gesendet",
sentTo: "{type} Transaktion gesendet nach",
timestamp: "Zeitstempel",
types: {
all: "Alles",
incoming: "Eingehend",
outgoing: "Ausgehend",
pending: "Ausstehend",
pendingIncoming: "Ausstehend eingehend",
pendingOutgoing: "Ausstehend ausgehend",
miner: "Miner",
serviceNode: "Service Node",
governance: "Governance",
stake: "Stake",
failed: "Fehlgeschlagen"
}
},
unspentOutputs: "Unspent outputs",
userNotUsedAddress: "Du hast diese Adresse nicht benutzt",
userUsedAddress: "Du hast diese Adresse benutzt",
viewKey: "View Key",
viewOnlyMode: "Nur Anzeige Modus. Bitte die volle Wallet laden um Coins zu senden"
noPassword: {
title: "Kein Passwort angelegt",
message: "Bist du sicher, dass du eine Wallet ohne Passwort erstellen möchtest?",
ok: "JA"
},
titles: {
addressBook: "Adressbuch",
addressDetails: "Adressdetails",
advanced: {
checkTransaction: "CHECK TRANSACTION",
prove: "PROVE"
},
changePassword: "Passwort ändern",
configure: "Konfiguaration",
currentlyStakedNodes: "Currently staked nodes",
privateKeys: "Private Keys",
rescanWallet: "Wallet erneut scannen",
serviceNode: {
registration: "REGISTRIERUNG",
staking: "STAKING"
},
settings: {
title: "Einstellungen",
tabs: {
general: "Allgemein",
language: "Sprache",
peers: "Peers"
}
},
transactionDetails: "Transaktionsdetails",
transactions: "Transaktionen",
wallet: {
createNew: "Neue Wallet erstellen",
createdOrRestored: "Wallet erstellt/wiederhergestellt",
importFromFile: "Importieren der Wallet aus Datei",
importFromLegacyGUI: "Wiederherstellung der Wallet von legacy GUI",
importFromOldGUI: "Wiederherstellung der Wallet von altem GUI",
restoreFromSeed: "Wiederherstellung Wallet von Seed Wörtern",
restoreViewOnly: "Wiederherstellung Anzeige Wallet"
},
welcome: "Willkommen",
yourWallets: "Deine Wallets"
password: {
title: "Passwort",
message: "Wallet Passwort eingeben um fortzufahren"
},
registerServiceNode: {
title: "Service Node registrieren",
message: "Möchtest du einen Service Node registrieren?",
ok: "REGISTRIEREN "
},
rescan: {
title: "Wallet erneut scannen",
message:
"Warnung: Einige Informationen über vorherige Transaktionen\nsowie Adressen von Empfängern gehen verloren",
ok: "ERNEUT SCANNEN"
},
restart: {
title: "Erneut starten",
message: "Änderungen erfordern einen Neustart. Möchtest du jetzt neu starten?",
ok: "NEUSTART"
},
showPrivateKeys: {
title: "Private Keys Anzeigen",
message: "Möchtest du deinen Private Key anzeigen?",
ok: "ANZEIGEN"
},
stake: {
title: "Stake",
message: "Möchtest du staken?",
ok: "Stake"
},
sweepAll: {
title: "Alles Bereinigen",
message: "Möchtest du alles bereinigen?",
ok: "ALLES BEREINIGEN"
},
sweepAllWarning: {
title: "Sweep all warning",
message:
"You are about to combine all of your unspent funds by sending a transaction to yourself, your wallet may show a balance of 0 temporarily, after 10 blocks your funds will unlock and you may stake normally.",
ok: "CONTINUE"
},
switchWallet: {
title: "Wallet wechseln",
closeMessage: "Bist du sicher, dass du die aktuelle Wallet schliessen möchtest?",
restartMessage:
"Die Wallet RPC synchronisiert sich gerade\n Wenn du deine Wallet wechseln möchtest, musst du die Anwendung erneut starten. \n Die Synchronisation wird abgebrochen und du musst die Blockchain erneut scannen. "
},
transactionDetails: {
title: "Transaktionsdetails",
ok: "SCHLIESSEN"
},
transfer: {
title: "Transferieren",
message: "Möchtest du die Transaktion senden?",
ok: "SENDEN"
},
unlockConfirm: {
title: "Unlock bestätigen",
ok: "UNLOCK"
},
unlockServiceNode: {
title: "Unlock Service Node",
confirmTitle: "Unlock bestätigen",
message: "Möchtest du den Service Node „unlocken“?",
ok: "UNLOCK"
},
unlockServiceNodeWarning: {
title: "Unlock service node warning",
message:
"Unlocking a partial stake in a node will also unstake for any other participants, if staking in a shared node its best to let the operator and other participants know you are unstaking.",
ok: "CONTINUE"
}
}
},
fieldLabels: {
// Field labels are also all uppercased
address: "ADRESSE",
amount: "BETRAG",
awardRecepientAddress: "EMPFÄNGER ADRESSE FÜR DIE VERGÜTUNG",
confirmPassword: "PASSWORT BESTÄTIGEN",
chooseNetwork: "NETZWERK AUSWÄHLEN",
daemonLogLevel: "DAEMON LOG LEVEL",
daemonP2pPort: "DAEMON P2P PORT",
daemonZMQPort: "DAEMON ZMQ PORT",
dataStoragePath: "DATENSICHERUNGSPFAD",
filter: "FILTER",
filterTransactionType: "FILTERN NACH TRANSAKTIONSTYP",
internalWalletPort: "INTERNER WALLET PORT",
keyImages: {
exportDirectory: "KEY IMAGE EXPORTVERZEICHNIS",
importFile: "KEY IMAGE IMPORT FILE"
},
limitDownloadRate: "DOWNLOAD RATE LIMITIEREN",
limitUploadRate: "UPLOAD RATE LIMITIEREN",
localDaemonIP: "LOKALE DAEMON IP",
localDaemonPort: "LOKALER DAEMON PORT",
maxIncomingPeers: "MAX EINGEHENDE PEERS",
maxOutgoingPeers: "MAX AUSGEHENDE PEERS",
message: "MESSAGE",
mnemonicSeed: "MNEMONIC SEED",
name: "NAME",
newWalletName: "NEUER WALLET NAME",
network: "NETZWERK",
notes: "NOTIZEN",
optional: "OPTIONAL",
password: "PASSWORT",
paymentId: "PAYMENT ID",
priority: "PRIORITÄT",
remoteNodeHost: "REMOTE NODE HOST",
remoteNodePort: "REMOTE NODE PORT",
restoreFromBlockHeight: "WIEDERHERSTELLUNG VON BLOCKHÖHE",
restoreFromDate: "WIEDERHERSTELLUNG VON DATUM",
seedLanguage: "SEED SPRACHE",
serviceNodeCommand: "SERVICE NODE COMMAND",
serviceNodeKey: "SERVICE NODE KEY",
signature: "SIGNATURE",
transactionId: "TRANSACTION ID",
walletFile: "WALLET FILE",
walletLogLevel: "WALLET LOG LEVEL",
walletName: "WALLET NAME",
walletRPCPort: "WALLET RPC PORT",
walletStoragePath: "WALLET SICHERUNGSPFAD",
// These are specific labels which do not get uppercased
confirmNewPassword: "Neues Passwort bestätigen",
newPassword: "Neues Passwort",
oldPassword: "Altes Passwort",
rescanFullBlockchain: "Gesamte Blockchain erneut scannen",
rescanSpentOutputs: "Spent Outputs erneut scannen",
transactionNotes: "Transaktionsnotizen"
},
footer: {
ready: "FERTIG",
remote: "REMOTE",
scanning: "SCANNEN",
status: "Status",
syncing: "Synchronisierung",
wallet: "Wallet"
},
menuItems: {
about: "Über ",
changePassword: "Passwort ändern",
copyAddress: "Adresse kopieren",
copyQR: "QR Code kopieren",
copySeedWords: "Seed Wörter kopieren",
copySpendKey: "Spend Key kopieren",
copyServiceNodeKey: "Copy service node key",
copyTransactionId: "Transaktions ID kopieren",
copyViewKey: "View Key kopieren",
createNewWallet: "Neue Wallet erstellen",
deleteWallet: "Wallet löschen",
exit: "Loki GUI Wallet schliessen",
importOldGUIWallet: "Wallets von alter GUI importieren",
manageKeyImages: "Key Images verwalten",
openWallet: "Wallet öffnen",
rescanWallet: "Wallet erneut scannen",
restoreWalletFile: "Wallet aus Datei wiederherstellen",
restoreWalletSeed: "Wallet aus Seed wiederherstellen ",
saveQR: "QR in Datei speichern",
sendToThisAddress: "Zu dieser Adresse senden",
settings: "Einstellungen",
showDetails: "Details anzeigen",
showPrivateKeys: "Zeige Private Keys",
showQRCode: "Zeige QR Code",
switchWallet: "Wallet wechseln",
viewOnExplorer: "Zeige in Explorer"
},
notification: {
positive: {
addressCopied: "Adresse in die Zwischenablage kopiert",
bannedPeer: "Blockiert {host} bis {time}",
copied: "{item} in Zwischenablage kopiert",
itemSaved: "{item} gespeichert nach {filename}",
keyImages: {
exported: "Key images exportiert nach {filename}",
imported: "Key images importiert"
},
passwordUpdated: "Passwort aktualisiert",
qrCopied: "QR Code in die Zwischenablage kopiert",
registerServiceNodeSuccess: "Service Node erfolgreich registriert ",
sendSuccess: "Transaktion erfolgreich gesendet",
signatureCopied: "Signature copied to clipboard",
stakeSuccess: "Staking erfolgreich",
transactionNotesSaved: "Notizen zur Transaktion gesichert"
},
errors: {
banningPeer: "Fehler bei der Blockierung des Peer",
cannotAccessRemoteNode: "Remote Node nicht erreichbar, bitte versuche es mit einer anderen Remote Node",
changingPassword: "Fehler beim Ändern des Passworts",
copyWalletFail: "Fehler beim Kopieren der Wallet",
copyingPrivateKeys: "Fehler beim Kopieren der Private Keys",
dataPathNotFound: "Pfad zur Speicherung nicht gefunden",
differentNetType: "Remote Node benutzt einen anderen „nettype“",
enterSeedWords: "Seed Wörter eingeben",
enterTransactionId: "Enter transaction ID",
enterTransactionProof: "Enter transaction proof",
enterWalletName: "Wallet Namen eingeben",
errorSavingItem: "Fehler beim Speichern {item}",
failedServiceNodeUnlock: "Fehler beim Service Node unlock",
failedToSetLanguage: "Fehler bei der Auswahl der Sprache: {lang}",
failedWalletImport: "Fehler beim Importieren der Wallet",
failedWalletOpen: "Fehler beim Öffnen der Wallet. Bitte versuche es erneut",
internalError: "Interner Fehler",
invalidAddress: "Adresse nicht gültig",
invalidAmount: "Betrag nicht gültig",
invalidOldPassword: "Ungültiges altes Passwort",
invalidPassword: "Ungültiges Passwort",
invalidPaymentId: "Payment ID nicht gültig",
invalidPrivateViewKey: "Ungültiger Private View Key",
invalidPublicAddress: "Ungültige öffentliche Adresse",
invalidRestoreDate: "Ungültiges Wiederherstellungsdatum",
invalidRestoreHeight: "Ungültige Wiederherstellungshöhe",
invalidSeedLength: "Ungültige Seed Wortlänge",
invalidServiceNodeCommand: "Bitte füge den Service Node Registrierungsbefehl ein",
invalidServiceNodeKey: "Service Node Key nicht gültig",
invalidWalletPath: "Ungültiger Wallet Pfad",
keyImages: {
exporting: "Fehler beim Export der Key images",
reading: "Fehler beim lesen der Key images",
importing: "Fehler beim Import der Key Images"
},
negativeAmount: "Betrag kann nicht negativ sein ",
newPasswordNoMatch: "Neue Passwörter stimmen nicht überein",
newPasswordSame: "Neues Passwort darf nicht identisch sein",
notEnoughBalance: "Nicht genug frei verfügbares Guthaben",
passwordNoMatch: "Passwörter stimmen nicht überein",
remoteCannotBeReached: "Remote daemon ist nicht erreichbar",
selectWalletFile: "Select a wallet file",
unknownError: "Ein unbekannter Fehler ist aufgetreten ",
walletAlreadyExists: "Wallet mit diesem Namen existiert bereits",
walletPathNotFound: "Wallet Daten Pfad nicht gefunden",
zeroAmount: "Betrag muss grösser als null sein"
},
warnings: {
noKeyImageExport: "Keine Key Images zum Exportieren gefunden",
usingLocalNode: "Zugang zur Remote Node nicht möglich, wechsle zur lokalen Node",
usingRemoteNode: "llokid nicht gefunden, benutze eine Remote Node"
}
},
placeholders: {
additionalNotes: "Zusätzliche Notizen",
addressBookName: "Zugehörige Namen zu dieser Adresse",
filterTx: "Enter an ID, name, address or amount",
hexCharacters: "{count} Hexadezimal Zeichen",
mnemonicSeed: "25 (oder 24) mnemonic Seed Wörter",
pasteTransactionId: "Paste transaction ID",
pasteTransactionProof: "Paste transaction proof",
proveOptionalMessage: "Optional message against which the signature is signed",
recipientWalletAddress: "Recipient's wallet address",
selectAFile: "Bitte Datei auswählen",
transactionNotes: "Zusätzliche Notizen die an die Transaktions gehängt werden sollen",
walletName: "Ein Name für deine Wallet",
walletPassword: "Ein optionales Passwort für die Wallet"
},
strings: {
addAddressBookEntry: "Adressbuch Eintrag hinzufügen",
addressBookDetails: "Adressbuch details",
addressBookIsEmpty: "Adressbuch ist leer",
addresses: {
myPrimaryAddress: "Meine primäre Adresse",
myUnusedAddresses: "Meine ungenutzten Adressen",
myUsedAddresses: "Meine benutzen Adressen",
primaryAddress: "Primäre Adresse",
subAddress: "Sub-Adresse",
subAddressIndex: "Index {index}"
},
advancedOptions: "Erweiterte Optionen",
bannedPeers: {
title: "Blockierte Peers (Blockierungen werden entfernt, wenn Wallet neu gestartet wird)",
bannedUntil: "Blockieren bis {time}"
},
blockHeight: "Höhe",
checkTransaction: {
description:
"Verify that funds were paid to an address by supplying the transaction ID, the recipient address, the message used for signing and the signature.\nFor a 'Spend Proof' you dont need to provide the recipient address.",
infoTitles: {
confirmations: "Confirmations",
inPool: "In pool",
validTransaction: "Valid transaction",
received: "Received amount"
},
validTransaction: {
no: "NO",
yes: "YES"
}
},
closing: "schliessen",
connectingToBackend: "Verbinden zum Backend",
contribution: "Contribution",
daemon: {
local: {
title: "Nur lokaler Daemon",
description:
"Volle Sicherheit. Wallet wird die gesamte Blockchain herunterladen. Du kannst keine Transaktionen durchführen, solange die Synchronisation nicht vollständig beendet wurde"
},
localRemote: {
title: "Lokal + Remote Daemon",
description:
"Schnell starten mit dieser Standard Option. Wallet wird die Blockchain vollständig herunterladen, aber während der Synchronisation eine Remote Node nutzen"
},
remote: {
title: "Nur Remote Node",
description:
"Etwas weniger sicher. Wallet verbindet sich mit einer Remote Node, um Transaktionen über diese durchzuführen"
}
},
destinationUnknown: "Ziel unbekannt",
editAddressBookEntry: "Adressbucheintrag bearbeiten",
loadingSettings: "Einstellungen werden geladen",
lokiBalance: "Guthaben",
lokiUnlockedBalance: "frei verfügbares Guthaben",
lokiUnlockedShort: "frei verfügbar",
noTransactionsFound: "Keine Transaktionen gefunden",
notes: "Notizen",
numberOfUnspentOutputs: "Anzahl der unspent outputs",
paymentID: "Payment ID",
peerList: "Peer Liste",
priorityOptions: {
automatic: "Automatisch",
fast: "Schnell",
fastest: "Am schnellsten",
normal: "Normal",
slow: "Langsam"
},
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: "Lese Wallet Liste",
recentIncomingTransactionsToAddress: "Kürzlich eingegangene Transaktionen zu dieser Adresse",
recentTransactionsWithAddress: "Kürzlich durchgeführte Transaktionen mit dieser Adresse",
rescanModalDescription: "Auswahl gesamter Rescan oder nur spent outputs",
saveSeedWarning: 'Bitte kopiere und verwahre deinen "Seed" an einem sicheren Ort',
saveToAddressBook: "In Adressbuch speichern",
seedWords: "Seed Wörter",
selectLanguage: "Sprache auswählen",
serviceNodeRegistrationDescription:
"Hier den {registerComand} Befehl, der mit Hilfe des Daemons durch das Kommando {prepareCommand} erzeugt wurde, eingeben, um eine Service Node zu aktivieren",
spendKey: "Spend key",
startingDaemon: "Daemon wird gestartet",
startingWallet: "Wallet wird gestartet",
switchToDateSelect: "Wechsel zur Selektion nach Datum",
switchToHeightSelect: "Wechsel zu Selektion nach Höhe",
transactionID: "Transaktions ID",
transactionConfirmed: "Transaktion bestätigt",
transactions: {
amount: "Betrag",
description: "{type} Transaktion",
fee: "Gebühr",
paidBySender: "Vom Absender bezahlt",
received: "Empfangen",
sent: "Gesendet",
sentTo: "{type} Transaktion gesendet nach",
timestamp: "Zeitstempel",
types: {
all: "Alles",
incoming: "Eingehend",
outgoing: "Ausgehend",
pending: "Ausstehend",
pendingIncoming: "Ausstehend eingehend",
pendingOutgoing: "Ausstehend ausgehend",
miner: "Miner",
serviceNode: "Service Node",
governance: "Governance",
stake: "Stake",
failed: "Fehlgeschlagen"
}
},
unspentOutputs: "Unspent outputs",
userNotUsedAddress: "Du hast diese Adresse nicht benutzt",
userUsedAddress: "Du hast diese Adresse benutzt",
viewKey: "View Key",
viewOnlyMode: "Nur Anzeige Modus. Bitte die volle Wallet laden um Coins zu senden"
},
titles: {
addressBook: "Adressbuch",
addressDetails: "Adressdetails",
advanced: {
checkTransaction: "CHECK TRANSACTION",
prove: "PROVE"
},
changePassword: "Passwort ändern",
configure: "Konfiguaration",
currentlyStakedNodes: "Currently staked nodes",
privateKeys: "Private Keys",
rescanWallet: "Wallet erneut scannen",
serviceNode: {
registration: "REGISTRIERUNG",
staking: "STAKING"
},
settings: {
title: "Einstellungen",
tabs: {
general: "Allgemein",
language: "Sprache",
peers: "Peers"
}
},
transactionDetails: "Transaktionsdetails",
transactions: "Transaktionen",
wallet: {
createNew: "Neue Wallet erstellen",
createdOrRestored: "Wallet erstellt/wiederhergestellt",
importFromFile: "Importieren der Wallet aus Datei",
importFromLegacyGUI: "Wiederherstellung der Wallet von legacy GUI",
importFromOldGUI: "Wiederherstellung der Wallet von altem GUI",
restoreFromSeed: "Wiederherstellung Wallet von Seed Wörtern",
restoreViewOnly: "Wiederherstellung Anzeige Wallet"
},
welcome: "Willkommen",
yourWallets: "Deine Wallets"
}
};

File diff suppressed because it is too large Load Diff

View File

@ -1,490 +1,501 @@
export default {
buttons: {
// All button text is uppercased in the gui
advanced: "ADVANCED",
all: "TODO",
back: "ATRÁS",
browse: "EXAMINAR",
cancel: "CANCELAR",
change: "MODIFICAR",
check: "CHECK",
clear: "CLEAR",
close: "CERRAR",
contacts: "CONTACTOS",
copyAddress: "COPIAR DIRECCIÓN",
copySignature: "COPY SIGNATURE",
createWallet: "CREAR MONEDERO",
delete: "ELIMINAR",
edit: "EDITAR",
export: "EXPORTAR",
generate: "GENERATE",
import: "IMPORTAR",
importWallet: "IMPORTAR MONEDERO | IMPORTAR MONEDEROS",
next: "SIGUIENTE",
openWallet: "ABRIR MONEDERO",
receive: "RECIBIR",
registerServiceNode: "REGISTRAR NODO DE SERVICIO",
rescan: "VOLVER A EXAMINAR",
restoreWallet: "RESTAURAR MONEDERO",
save: "GUARDAR",
saveTxNotes: "GUARDAR NOTAS DE LA TRANSACCIÓN",
selectLocation: "SELECCIONAR UBICACIÓN",
selectWalletFile: "SELECCIONAR ARCHIVO DEL MONEDERO",
send: "ENVIAR",
sendCoins: "ENVIAR MONEDAS",
serviceNode: "NODO DE SERVICIO",
settings: "CONFIGURACIÓN",
showQRCode: "MOSTRAR CÓDIGO QR",
showTxDetails: "MOSTRAR DETALLES DE LA TRANSACCIÓN",
stake: "RETENER PARTICIPACIÓN",
sweepAll: "TRANSFERIR/BARRER TODO",
unlock: "UNLOCK",
viewOnExplorer: "VER EN EL EXPLORADOR"
},
dialog: {
// Generic buttons
buttons: {
// All button text is uppercased in the gui
advanced: "ADVANCED",
all: "TODO",
back: "ATRÁS",
browse: "EXAMINAR",
cancel: "CANCELAR",
change: "MODIFICAR",
check: "CHECK",
clear: "CLEAR",
close: "CERRAR",
contacts: "CONTACTOS",
copyAddress: "COPIAR DIRECCIÓN",
copySignature: "COPY SIGNATURE",
createWallet: "CREAR MONEDERO",
delete: "ELIMINAR",
edit: "EDITAR",
export: "EXPORTAR",
generate: "GENERATE",
import: "IMPORTAR",
importWallet: "IMPORTAR MONEDERO | IMPORTAR MONEDEROS",
next: "SIGUIENTE",
openWallet: "ABRIR MONEDERO",
receive: "RECIBIR",
registerServiceNode: "REGISTRAR NODO DE SERVICIO",
rescan: "VOLVER A EXAMINAR",
restoreWallet: "RESTAURAR MONEDERO",
save: "GUARDAR",
saveTxNotes: "GUARDAR NOTAS DE LA TRANSACCIÓN",
selectLocation: "SELECCIONAR UBICACIÓN",
selectWalletFile: "SELECCIONAR ARCHIVO DEL MONEDERO",
send: "ENVIAR",
sendCoins: "ENVIAR MONEDAS",
serviceNode: "NODO DE SERVICIO",
settings: "CONFIGURACIÓN",
showQRCode: "MOSTRAR CÓDIGO QR",
showTxDetails: "MOSTRAR DETALLES DE LA TRANSACCIÓN",
stake: "RETENER PARTICIPACIÓN",
sweepAll: "TRANSFERIR/BARRER TODO",
unlock: "UNLOCK",
viewOnExplorer: "VER EN EL EXPLORADOR"
ok: "ACEPTAR",
cancel: "CANCELAR",
open: "ABRIR"
},
dialog: {
// Generic buttons
buttons: {
ok: "ACEPTAR",
cancel: "CANCELAR",
open: "ABRIR"
},
// Dialogs
banPeer: {
title: "Vetar pares",
peerDetailsTitle: "Detalles del par",
message: "Introduzca el periodo de veto del par en segundos.\nPor defecto 3600 = 1 hora.",
ok: "Vetar par"
},
copyAddress: {
title: "Copiar dirección",
message: "Hay un id de pago asociado a esta dirección.\nAsegúrese de copiar también el id de pago por separado."
},
copyPrivateKeys: {
// Copy {seedWords/viewKey/spendKey}
title: "Copiar {type}",
message: "Tenga cuidado si comparte sus claves privadas ya que estas permiten controlar sus fondos.",
seedWords: "Palabras Semilla",
viewKey: "Clave de Visualización",
spendKey: "Clave de Gasto"
},
deleteWallet: {
title: "Eliminar monedero",
message: "¿Está absolutamente seguro de querer eliminar su monedero?\nAsegúrese de tener una copia de seguridad de sus claves privadas antes de proceder.\n¡ESTE PROCESO NO ES REVERSIBLE!",
ok: "ELIMINAR"
},
exit: {
title: "Salir",
message: "¿Está seguro de que desea salir?",
ok: "SALIR"
},
keyImages: {
title: "{type} imágenes de la clave",
message: "¿Desea {type} las imágenes de la clave?",
export: "Exportar",
import: "Importar"
},
noPassword: {
title: "Sin contraseña",
message: "¿Está seguro de que quiere crear un monedero sin contraseña?",
ok: "SÍ"
},
password: {
title: "Contraseña",
message: "Introduzca la contraseña del monedero para continuar."
},
registerServiceNode: {
title: "Registrar nodo de servicio",
message: "¿Desea registrar el nodo de servicio?",
ok: "REGISTRAR"
},
rescan: {
title: "Volver a examinar el monedero",
message: "Advertencia: Cierta información de las transacciones\nanteriores, como la dirección del destinatario, se perderá.",
ok: "VOLVER A EXAMINAR"
},
restart: {
title: "Reiniciar",
message: "Los cambios requieren reiniciar la aplicación ¿Desea reiniciar ahora?",
ok: "REINICIAR"
},
showPrivateKeys: {
title: "Mostrar claves privadas",
message: "¿Desea ver sus claves privadas?",
ok: "MOSTRAR"
},
stake: {
title: "RETENER PARTICIPACIÓN",
message: "¿Desea participar reteniendo la cantidad que ha indicado?",
ok: "RETENER"
},
sweepAll: {
title: "Transferir/Barrer todo",
message: "¿Está seguro de que quiere transferir/barrer todos sus fondos?",
ok: "TRANSFERIR/BARRER TODO"
},
sweepAllWarning: {
title: "Sweep all warning",
message: "You are about to combine all of your unspent funds by sending a transaction to yourself, your wallet may show a balance of 0 temporarily, after 10 blocks your funds will unlock and you may stake normally.",
ok: "CONTINUE"
},
switchWallet: {
title: "Cambiar de monedero",
closeMessage: "¿Confirma que desea cerrar el monedero actual?",
restartMessage: "El monedero se está sincronizando. \nSi desea cambiar de monedero, tendrá que reiniciar la aplicación. \nEl proceso de sincronización se perderá y será necesario volver a examinar la cadena de bloques."
},
transactionDetails: {
title: "Detalles de la transacción",
ok: "CERRAR"
},
transfer: {
title: "Transferir",
message: "¿Desea enviar la transacción?",
ok: "ENVIAR"
},
unlockConfirm: {
title: "Confirmar liberación",
ok: "LIBERAR"
},
unlockServiceNode: {
title: "Liberar nodo de servicio",
confirmTitle: "Confirmar liberación",
message: "¿Desea liberar el nodo de servicio?",
ok: "LIBERAR"
},
unlockServiceNodeWarning: {
title: "Unlock service node warning",
message: "Unlocking a partial stake in a node will also unstake for any other participants, if staking in a shared node its best to let the operator and other participants know you are unstaking.",
ok: "CONTINUE"
}
// Dialogs
banPeer: {
title: "Vetar pares",
peerDetailsTitle: "Detalles del par",
message: "Introduzca el periodo de veto del par en segundos.\nPor defecto 3600 = 1 hora.",
ok: "Vetar par"
},
fieldLabels: {
// Field labels are also all uppercased
address: "DIRECCIÓN",
amount: "CANTIDAD",
confirmPassword: "CONFIRMAR CONTRASEÑA",
daemonLogLevel: "NIVEL LOG PARA EL SERVICIO",
daemonP2pPort: "PUERTO P2P SERVICIO",
daemonZMQPort: "PUERTO ZMQ SERVICIO",
dataStoragePath: "RUTA DE ALMACENAMIENTO DE DATOS",
filter: "FILTER",
filterTransactionType: "FILTRAR POR TIPO DE TRANSACCIÓN",
internalWalletPort: "PUERTO INTERNO MONEDERO",
keyImages: {
exportDirectory: "DIRECTORIO AL QUE EXPORTAR LA IMAGEN DE LA CLAVE",
importFile: "ARCHIVO DE IMPORTACIÓN DE LA IMAGEN DE LA CLAVE"
},
limitDownloadRate: "LÍMITE VELOCIDAD DE DESCARGA",
limitUploadRate: "LÍMITE VELOCIDAD DE SUBIDA",
localDaemonIP: "IP SERVICIO LOCAL",
localDaemonPort: "PUERTO SERVICIO LOCAL",
maxIncomingPeers: "NÚM. MÁX. PARES ENTRANTES",
maxOutgoingPeers: "NÚM. MÁX. PARES SALIENTES",
message: "MESSAGE",
mnemonicSeed: "SEMILLA MNEMÓNICA",
name: "NOMBRE",
newWalletName: "NUEVO NOMBRE PARA EL MONEDERO",
notes: "NOTAS",
optional: "OPCIONAL",
password: "CONTRASEÑA",
paymentId: "ID DE PAGO",
priority: "PRIORIDAD",
remoteNodeHost: "HOST NODO REMOTO",
remoteNodePort: "PUERTO NODO REMOTO",
restoreFromBlockHeight: "RESTAURAR DESDE EL BLOQUE NÚMERO",
restoreFromDate: "RESTAURAR DESDE EL DÍA",
seedLanguage: "IDIOMA SEMILLA",
serviceNodeCommand: "ORDEN PARA REGISTRAR EL NODO DE SERVICIO",
serviceNodeKey: "CLAVE NODO DE SERVICIO",
signature: "SIGNATURE",
transactionId: "TRANSACTION ID",
walletFile: "ARCHIVO MONEDERO",
walletLogLevel: "NIVEL LOG MONEDERO",
walletName: "NOMBRE MONEDERO",
walletRPCPort: "PUERTO RPC MONEDERO",
walletStoragePath: "RUTA DE ALMACENAMIENTO DEL MONEDERO",
// These are specific labels which do not get uppercased
confirmNewPassword: "Confirme la nueva contraseña",
newPassword: "Contraseña nueva",
oldPassword: "Contraseña antigua",
rescanFullBlockchain: "Volver a examinar la cadena de bloques entera",
rescanSpentOutputs: "Volver a examinar las salidas gastadas",
transactionNotes: "Notas de la Transacción",
chooseNetwork: "Seleccione una Red",
network: "Red"
copyAddress: {
title: "Copiar dirección",
message: "Hay un id de pago asociado a esta dirección.\nAsegúrese de copiar también el id de pago por separado."
},
footer: {
ready: "LISTO",
scanning: "EXAMINANDO",
status: "Estado",
syncing: "SINCRONIZANDO",
remote: "Remoto",
wallet: "Monedero"
copyPrivateKeys: {
// Copy {seedWords/viewKey/spendKey}
title: "Copiar {type}",
message: "Tenga cuidado si comparte sus claves privadas ya que estas permiten controlar sus fondos.",
seedWords: "Palabras Semilla",
viewKey: "Clave de Visualización",
spendKey: "Clave de Gasto"
},
menuItems: {
about: "Acerca de",
changePassword: "Modificar Contraseña",
copyAddress: "Copiar dirección",
copyQR: "Copiar código QR",
copySeedWords: "Copiar palabras semilla",
copySpendKey: "Copiar clave de gasto",
copyServiceNodeKey: "Copy service node key",
copyTransactionId: "Copiar ID de la transacción",
copyViewKey: "Copiar clave de visualización",
createNewWallet: "Crear nuevo monedero",
deleteWallet: "Eliminar monedero",
exit: "Cerrar la interfaz del monedero Loki",
importOldGUIWallet: "Importar monedero de una interfaz gráfica antigua",
manageKeyImages: "Administrar Imágenes de Clave",
openWallet: "Abrir monedero",
rescanWallet: "Volver a examinar monedero",
restoreWalletFile: "Restaurar monedero de un archivo",
restoreWalletSeed: "Restaurar monedero mediante semilla",
saveQR: "Guardar código QR en archivo",
sendToThisAddress: "Enviar a esta dirección",
settings: "Configuración",
showDetails: "Mostrar detalles",
showPrivateKeys: "Mostrar Claves Privadas",
showQRCode: "Mostrar código QR",
switchWallet: "Cambiar de monedero",
viewOnExplorer: "Ver en el explorador"
deleteWallet: {
title: "Eliminar monedero",
message:
"¿Está absolutamente seguro de querer eliminar su monedero?\nAsegúrese de tener una copia de seguridad de sus claves privadas antes de proceder.\n¡ESTE PROCESO NO ES REVERSIBLE!",
ok: "ELIMINAR"
},
notification: {
positive: {
addressCopied: "Dirección copiada al portapapeles",
bannedPeer: "{host} vetado hasta {time}",
copied: "{item} copiado/a al portapapeles",
itemSaved: "{item} guardado/a en {filename}",
keyImages: {
exported: "Imágenes de clave exportadas a {filename}",
imported: "Imágenes de clave importadas"
},
passwordUpdated: "Contraseña actualizada",
qrCopied: "Código QR copiado al portapapeles",
registerServiceNodeSuccess: "Nodo de servicio registrado correctamente",
sendSuccess: "Transacción enviada correctamente",
signatureCopied: "Signature copied to clipboard",
stakeSuccess: "Participación retenida correctamente",
transactionNotesSaved: "Notas de la transacción guardadas"
},
errors: {
banningPeer: "Error al vetar el par",
cannotAccessRemoteNode: "No ha sido posible acceder al nodo remoto. Tenga la amabilidad de probar con otro",
changingPassword: "Error al cambiar la contraseña",
copyWalletFail: "Error al copiar el monedero",
copyingPrivateKeys: "Error al copiar las claves privadas",
dataPathNotFound: "No se ha encontrado la ruta de almacenamiento",
differentNetType: "El nodo remoto usa un tipo de red diferente",
enterSeedWords: "Introduzca las palabras semilla",
enterTransactionId: "Enter transaction ID",
enterTransactionProof: "Enter transaction proof",
enterWalletName: "Introduzca un nombre para el monedero",
errorSavingItem: "Error al guardar {item}",
failedServiceNodeUnlock: "La liberación del nodo de servicio ha fallado",
failedToSetLanguage: "Fallo al cambiar de idioma: {lang}",
failedWalletImport: "Fallo al importar el monedero",
failedWalletOpen: "Fallo al abrir el monedero. Inténtelo de nuevo.",
internalError: "Error interno",
invalidAddress: "Dirección no válida",
invalidAmount: "Cantidad no válida",
invalidOldPassword: "Contraseña antigua incorrecta",
invalidPassword: "Contraseña incorrecta",
invalidPaymentId: "id de pago no válido",
invalidPrivateViewKey: "Clave de visualización privada no válida",
invalidPublicAddress: "Dirección pública no válida",
invalidRestoreDate: "Fecha de restauración no válida",
invalidRestoreHeight: "Altura de bloque no válida",
invalidSeedLength: "Cantidad de palabras en la semilla incorrecta",
invalidServiceNodeCommand: "Por favor, introduzca el comando para registrar el nodo de servicio",
invalidServiceNodeKey: "Clave de nodo de servicio no válida",
invalidWalletPath: "Ruta del monedero no válida",
keyImages: {
exporting: "Error exportando imágenes de clave",
reading: "Error leyendo imágenes de clave",
importing: "Error importando imágenes de clave"
},
negativeAmount: "La cantidad no puede ser negativa",
newPasswordNoMatch: "Las contraseñas nuevas no coinciden",
newPasswordSame: "La nueva contraseña debe ser diferente",
notEnoughBalance: "No hay suficiente saldo libre",
passwordNoMatch: "Las contraseñas no coinciden",
remoteCannotBeReached: "No se puede conectar con el servicio remoto",
selectWalletFile: "Select a wallet file",
unknownError: "Ha ocurrido un error inesperado",
walletAlreadyExists: "Ya existe un monedero con este nombre",
walletPathNotFound: "Ruta no encontrada",
zeroAmount: "La cantidad debe ser mayor que cero"
},
warnings: {
noKeyImageExport: "No se han encontrado claves para exportar",
usingLocalNode: "No se ha podido acceder al nodo remoto, volviendo al modo local",
usingRemoteNode: "lokid no encontrado, utilizando nodo remoto"
}
exit: {
title: "Salir",
message: "¿Está seguro de que desea salir?",
ok: "SALIR"
},
placeholders: {
additionalNotes: "Notas adicionales",
addressBookName: "Nombre asociado a esta dirección",
filterTx: "Enter an ID, name, address or amount",
hexCharacters: "{count} caracteres hexadecimales",
mnemonicSeed: "Semilla mnemónica de 25 (o 24) palabras",
pasteTransactionId: "Paste transaction ID",
pasteTransactionProof: "Paste transaction proof",
proveOptionalMessage: "Optional message against which the signature is signed",
recipientWalletAddress: "Recipient's wallet address",
selectAFile: "Seleccione un archivo por favor",
transactionNotes: "Notas adicionales para agregar a la transacción",
walletName: "Nombre para identificar su monedero",
walletPassword: "Contraseña opcional para proteger su monedero"
keyImages: {
title: "{type} imágenes de la clave",
message: "¿Desea {type} las imágenes de la clave?",
export: "Exportar",
import: "Importar"
},
strings: {
addAddressBookEntry: "Agregar registro a la libreta de direcciones",
addressBookDetails: "Detalles de la libreta de direcciones",
addressBookIsEmpty: "La libreta de direcciones está vacía",
addresses: {
myPrimaryAddress: "Mi dirección principal",
myUnusedAddresses: "Mis direcciones no usadas",
myUsedAddresses: "Mis direcciones usadas",
primaryAddress: "Dirección principal",
subAddress: "Dirección auxiliar",
subAddressIndex: "Índice {index}"
},
advancedOptions: "Opciones avanzadas",
bannedPeers: {
title: "Pares vetados (los vetos se anularán si reinicia el monedero)",
bannedUntil: "Vetado hasta {time}"
},
blockHeight: "Altura",
checkTransaction: {
description: "Verify that funds were paid to an address by supplying the transaction ID, the recipient address, the message used for signing and the signature.\nFor a 'Spend Proof' you dont need to provide the recipient address.",
infoTitles: {
confirmations: "Confirmations",
inPool: "In pool",
validTransaction: "Valid transaction",
received: "Received amount"
},
validTransaction: {
no: "NO",
yes: "YES"
}
},
closing: "Cerrando",
connectingToBackend: "Conectando con el servicio",
contribution: "Contribution",
daemon: {
local: {
title: "Solo Servicio Local",
description: "Seguridad completa. Se descargará la cadena de bloques entera. No podrá operar hasta que finalice el proceso de sincronización."
},
localRemote: {
title: "Servicio Local + Remoto",
description: "Empiece a operar rápidamente gracias a esta opción predeterminada. Se descargará la cadena de bloques al completo pero se usará un nodo remoto mientras dure la descarga."
},
remote: {
title: "Solo Servicio Remoto",
description: "Menor seguridad. El monedero se conectará a un nodo remoto para realizar cualquier operación."
}
},
destinationUnknown: "Destino Desconocido",
editAddressBookEntry: "Modificar un registro de la libreta de direcciones",
loadingSettings: "Cargando configuración",
lokiBalance: "Saldo",
lokiUnlockedBalance: "Saldo libre",
lokiUnlockedShort: "Libre",
noTransactionsFound: "No se han encontrado transacciones",
notes: "Notas",
numberOfUnspentOutputs: "Número de salidas no gastadas",
paymentID: "ID de pago",
priorityOptions: {
automatic: "Automática",
slow: "Lenta",
normal: "Normal",
fast: "Rápida",
fastest: "La más rápida"
},
peerList: "Lista de pares",
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: "Leyendo el listado de monederos",
recentIncomingTransactionsToAddress: "Transacciones recientes recibidas en esta dirección",
recentTransactionsWithAddress: "Transacciones recientes con esta dirección",
rescanModalDescription: "Escoja entre un examen completo o examinar solo las salidas gastadas.",
saveSeedWarning: "¡Por favor, cópielas y guárdelas en un sitio seguro!",
saveToAddressBook: "Guardar en la libreta de direcciones",
seedWords: "Palabras semilla",
selectLanguage: "Escoja un idioma",
serviceNodeRegistrationDescription: "Introduzca la orden {registerCommand} generada por el servicio (lokid) que se está intentado registrar como Nodo de Servicio usando la instrucción \"{prepareCommand}\"",
spendKey: "Clave de gasto",
startingDaemon: "Iniciando servicio",
startingWallet: "Iniciando monedero",
switchToDateSelect: "Cambiar a selección por fecha",
switchToHeightSelect: "Cambiar a selección por altura",
transactionID: "ID de la transacción",
transactionConfirmed: "confirmada",
transactions: {
amount: "Cantidad",
description: "Transacción {type}",
fee: "Comisión",
paidBySender: "pagada por el remitente",
received: "Recibida",
sent: "Enviada",
sentTo: "Transacción {type} enviada a",
timestamp: "Fecha y hora",
types: {
all: "Todas",
incoming: "Recibida",
outgoing: "Emitida",
pending: "Pendiente",
pendingIncoming: "Recibida pendiente",
pendingOutgoing: "Emitida pendiente",
miner: "Minería",
serviceNode: "Nodo de Servicio",
governance: "Gobernanza",
stake: "Retención participación",
failed: "Fallida"
}
},
unspentOutputs: "Salidas no gastadas",
userNotUsedAddress: "No ha utilizado esta dirección",
userUsedAddress: "Ha utilizado esta dirección",
viewKey: "Clave de visualización",
viewOnlyMode: "Este monedero solo permite visualizar operaciones. Por favor, abra uno completo para poder transferir fondos."
noPassword: {
title: "Sin contraseña",
message: "¿Está seguro de que quiere crear un monedero sin contraseña?",
ok: "SÍ"
},
titles: {
addressBook: "Libreta de direcciones",
addressDetails: "Detalles de la dirección",
advanced: {
checkTransaction: "CHECK TRANSACTION",
prove: "PROVE"
},
changePassword: "Modificar contraseña",
configure: "Configurar",
currentlyStakedNodes: "Currently staked nodes",
privateKeys: "Claves privadas",
rescanWallet: "Volver a examinar monedero",
serviceNode: {
registration: "REGISTRO",
staking: "PARTICIPACIÓN"
},
settings: {
title: "Configuración",
tabs: {
general: "General",
language: "Idioma",
peers: "Pares"
}
},
transactionDetails: "Detalles de la transacción",
transactions: "Transacciones",
wallet: {
createNew: "Crear un monedero nuevo",
createdOrRestored: "Monedero creado/restaurado",
importFromFile: "Importar monedero de un archivo",
importFromLegacyGUI: "Importar monedero de una interfaz gráfica heredada",
importFromOldGUI: "Importar monedero de una interfaz gráfica antigua",
restoreFromSeed: "Restaurar monedero mediante semilla",
restoreViewOnly: "Restaurar monedero de solo visualización"
},
welcome: "Bienvenido/a",
yourWallets: "Sus Monederos"
password: {
title: "Contraseña",
message: "Introduzca la contraseña del monedero para continuar."
},
registerServiceNode: {
title: "Registrar nodo de servicio",
message: "¿Desea registrar el nodo de servicio?",
ok: "REGISTRAR"
},
rescan: {
title: "Volver a examinar el monedero",
message:
"Advertencia: Cierta información de las transacciones\nanteriores, como la dirección del destinatario, se perderá.",
ok: "VOLVER A EXAMINAR"
},
restart: {
title: "Reiniciar",
message: "Los cambios requieren reiniciar la aplicación ¿Desea reiniciar ahora?",
ok: "REINICIAR"
},
showPrivateKeys: {
title: "Mostrar claves privadas",
message: "¿Desea ver sus claves privadas?",
ok: "MOSTRAR"
},
stake: {
title: "RETENER PARTICIPACIÓN",
message: "¿Desea participar reteniendo la cantidad que ha indicado?",
ok: "RETENER"
},
sweepAll: {
title: "Transferir/Barrer todo",
message: "¿Está seguro de que quiere transferir/barrer todos sus fondos?",
ok: "TRANSFERIR/BARRER TODO"
},
sweepAllWarning: {
title: "Sweep all warning",
message:
"You are about to combine all of your unspent funds by sending a transaction to yourself, your wallet may show a balance of 0 temporarily, after 10 blocks your funds will unlock and you may stake normally.",
ok: "CONTINUE"
},
switchWallet: {
title: "Cambiar de monedero",
closeMessage: "¿Confirma que desea cerrar el monedero actual?",
restartMessage:
"El monedero se está sincronizando. \nSi desea cambiar de monedero, tendrá que reiniciar la aplicación. \nEl proceso de sincronización se perderá y será necesario volver a examinar la cadena de bloques."
},
transactionDetails: {
title: "Detalles de la transacción",
ok: "CERRAR"
},
transfer: {
title: "Transferir",
message: "¿Desea enviar la transacción?",
ok: "ENVIAR"
},
unlockConfirm: {
title: "Confirmar liberación",
ok: "LIBERAR"
},
unlockServiceNode: {
title: "Liberar nodo de servicio",
confirmTitle: "Confirmar liberación",
message: "¿Desea liberar el nodo de servicio?",
ok: "LIBERAR"
},
unlockServiceNodeWarning: {
title: "Unlock service node warning",
message:
"Unlocking a partial stake in a node will also unstake for any other participants, if staking in a shared node its best to let the operator and other participants know you are unstaking.",
ok: "CONTINUE"
}
}
},
fieldLabels: {
// Field labels are also all uppercased
address: "DIRECCIÓN",
amount: "CANTIDAD",
confirmPassword: "CONFIRMAR CONTRASEÑA",
daemonLogLevel: "NIVEL LOG PARA EL SERVICIO",
daemonP2pPort: "PUERTO P2P SERVICIO",
daemonZMQPort: "PUERTO ZMQ SERVICIO",
dataStoragePath: "RUTA DE ALMACENAMIENTO DE DATOS",
filter: "FILTER",
filterTransactionType: "FILTRAR POR TIPO DE TRANSACCIÓN",
internalWalletPort: "PUERTO INTERNO MONEDERO",
keyImages: {
exportDirectory: "DIRECTORIO AL QUE EXPORTAR LA IMAGEN DE LA CLAVE",
importFile: "ARCHIVO DE IMPORTACIÓN DE LA IMAGEN DE LA CLAVE"
},
limitDownloadRate: "LÍMITE VELOCIDAD DE DESCARGA",
limitUploadRate: "LÍMITE VELOCIDAD DE SUBIDA",
localDaemonIP: "IP SERVICIO LOCAL",
localDaemonPort: "PUERTO SERVICIO LOCAL",
maxIncomingPeers: "NÚM. MÁX. PARES ENTRANTES",
maxOutgoingPeers: "NÚM. MÁX. PARES SALIENTES",
message: "MESSAGE",
mnemonicSeed: "SEMILLA MNEMÓNICA",
name: "NOMBRE",
newWalletName: "NUEVO NOMBRE PARA EL MONEDERO",
notes: "NOTAS",
optional: "OPCIONAL",
password: "CONTRASEÑA",
paymentId: "ID DE PAGO",
priority: "PRIORIDAD",
remoteNodeHost: "HOST NODO REMOTO",
remoteNodePort: "PUERTO NODO REMOTO",
restoreFromBlockHeight: "RESTAURAR DESDE EL BLOQUE NÚMERO",
restoreFromDate: "RESTAURAR DESDE EL DÍA",
seedLanguage: "IDIOMA SEMILLA",
serviceNodeCommand: "ORDEN PARA REGISTRAR EL NODO DE SERVICIO",
serviceNodeKey: "CLAVE NODO DE SERVICIO",
signature: "SIGNATURE",
transactionId: "TRANSACTION ID",
walletFile: "ARCHIVO MONEDERO",
walletLogLevel: "NIVEL LOG MONEDERO",
walletName: "NOMBRE MONEDERO",
walletRPCPort: "PUERTO RPC MONEDERO",
walletStoragePath: "RUTA DE ALMACENAMIENTO DEL MONEDERO",
// These are specific labels which do not get uppercased
confirmNewPassword: "Confirme la nueva contraseña",
newPassword: "Contraseña nueva",
oldPassword: "Contraseña antigua",
rescanFullBlockchain: "Volver a examinar la cadena de bloques entera",
rescanSpentOutputs: "Volver a examinar las salidas gastadas",
transactionNotes: "Notas de la Transacción",
chooseNetwork: "Seleccione una Red",
network: "Red"
},
footer: {
ready: "LISTO",
scanning: "EXAMINANDO",
status: "Estado",
syncing: "SINCRONIZANDO",
remote: "Remoto",
wallet: "Monedero"
},
menuItems: {
about: "Acerca de",
changePassword: "Modificar Contraseña",
copyAddress: "Copiar dirección",
copyQR: "Copiar código QR",
copySeedWords: "Copiar palabras semilla",
copySpendKey: "Copiar clave de gasto",
copyServiceNodeKey: "Copy service node key",
copyTransactionId: "Copiar ID de la transacción",
copyViewKey: "Copiar clave de visualización",
createNewWallet: "Crear nuevo monedero",
deleteWallet: "Eliminar monedero",
exit: "Cerrar la interfaz del monedero Loki",
importOldGUIWallet: "Importar monedero de una interfaz gráfica antigua",
manageKeyImages: "Administrar Imágenes de Clave",
openWallet: "Abrir monedero",
rescanWallet: "Volver a examinar monedero",
restoreWalletFile: "Restaurar monedero de un archivo",
restoreWalletSeed: "Restaurar monedero mediante semilla",
saveQR: "Guardar código QR en archivo",
sendToThisAddress: "Enviar a esta dirección",
settings: "Configuración",
showDetails: "Mostrar detalles",
showPrivateKeys: "Mostrar Claves Privadas",
showQRCode: "Mostrar código QR",
switchWallet: "Cambiar de monedero",
viewOnExplorer: "Ver en el explorador"
},
notification: {
positive: {
addressCopied: "Dirección copiada al portapapeles",
bannedPeer: "{host} vetado hasta {time}",
copied: "{item} copiado/a al portapapeles",
itemSaved: "{item} guardado/a en {filename}",
keyImages: {
exported: "Imágenes de clave exportadas a {filename}",
imported: "Imágenes de clave importadas"
},
passwordUpdated: "Contraseña actualizada",
qrCopied: "Código QR copiado al portapapeles",
registerServiceNodeSuccess: "Nodo de servicio registrado correctamente",
sendSuccess: "Transacción enviada correctamente",
signatureCopied: "Signature copied to clipboard",
stakeSuccess: "Participación retenida correctamente",
transactionNotesSaved: "Notas de la transacción guardadas"
},
errors: {
banningPeer: "Error al vetar el par",
cannotAccessRemoteNode: "No ha sido posible acceder al nodo remoto. Tenga la amabilidad de probar con otro",
changingPassword: "Error al cambiar la contraseña",
copyWalletFail: "Error al copiar el monedero",
copyingPrivateKeys: "Error al copiar las claves privadas",
dataPathNotFound: "No se ha encontrado la ruta de almacenamiento",
differentNetType: "El nodo remoto usa un tipo de red diferente",
enterSeedWords: "Introduzca las palabras semilla",
enterTransactionId: "Enter transaction ID",
enterTransactionProof: "Enter transaction proof",
enterWalletName: "Introduzca un nombre para el monedero",
errorSavingItem: "Error al guardar {item}",
failedServiceNodeUnlock: "La liberación del nodo de servicio ha fallado",
failedToSetLanguage: "Fallo al cambiar de idioma: {lang}",
failedWalletImport: "Fallo al importar el monedero",
failedWalletOpen: "Fallo al abrir el monedero. Inténtelo de nuevo.",
internalError: "Error interno",
invalidAddress: "Dirección no válida",
invalidAmount: "Cantidad no válida",
invalidOldPassword: "Contraseña antigua incorrecta",
invalidPassword: "Contraseña incorrecta",
invalidPaymentId: "id de pago no válido",
invalidPrivateViewKey: "Clave de visualización privada no válida",
invalidPublicAddress: "Dirección pública no válida",
invalidRestoreDate: "Fecha de restauración no válida",
invalidRestoreHeight: "Altura de bloque no válida",
invalidSeedLength: "Cantidad de palabras en la semilla incorrecta",
invalidServiceNodeCommand: "Por favor, introduzca el comando para registrar el nodo de servicio",
invalidServiceNodeKey: "Clave de nodo de servicio no válida",
invalidWalletPath: "Ruta del monedero no válida",
keyImages: {
exporting: "Error exportando imágenes de clave",
reading: "Error leyendo imágenes de clave",
importing: "Error importando imágenes de clave"
},
negativeAmount: "La cantidad no puede ser negativa",
newPasswordNoMatch: "Las contraseñas nuevas no coinciden",
newPasswordSame: "La nueva contraseña debe ser diferente",
notEnoughBalance: "No hay suficiente saldo libre",
passwordNoMatch: "Las contraseñas no coinciden",
remoteCannotBeReached: "No se puede conectar con el servicio remoto",
selectWalletFile: "Select a wallet file",
unknownError: "Ha ocurrido un error inesperado",
walletAlreadyExists: "Ya existe un monedero con este nombre",
walletPathNotFound: "Ruta no encontrada",
zeroAmount: "La cantidad debe ser mayor que cero"
},
warnings: {
noKeyImageExport: "No se han encontrado claves para exportar",
usingLocalNode: "No se ha podido acceder al nodo remoto, volviendo al modo local",
usingRemoteNode: "lokid no encontrado, utilizando nodo remoto"
}
},
placeholders: {
additionalNotes: "Notas adicionales",
addressBookName: "Nombre asociado a esta dirección",
filterTx: "Enter an ID, name, address or amount",
hexCharacters: "{count} caracteres hexadecimales",
mnemonicSeed: "Semilla mnemónica de 25 (o 24) palabras",
pasteTransactionId: "Paste transaction ID",
pasteTransactionProof: "Paste transaction proof",
proveOptionalMessage: "Optional message against which the signature is signed",
recipientWalletAddress: "Recipient's wallet address",
selectAFile: "Seleccione un archivo por favor",
transactionNotes: "Notas adicionales para agregar a la transacción",
walletName: "Nombre para identificar su monedero",
walletPassword: "Contraseña opcional para proteger su monedero"
},
strings: {
addAddressBookEntry: "Agregar registro a la libreta de direcciones",
addressBookDetails: "Detalles de la libreta de direcciones",
addressBookIsEmpty: "La libreta de direcciones está vacía",
addresses: {
myPrimaryAddress: "Mi dirección principal",
myUnusedAddresses: "Mis direcciones no usadas",
myUsedAddresses: "Mis direcciones usadas",
primaryAddress: "Dirección principal",
subAddress: "Dirección auxiliar",
subAddressIndex: "Índice {index}"
},
advancedOptions: "Opciones avanzadas",
bannedPeers: {
title: "Pares vetados (los vetos se anularán si reinicia el monedero)",
bannedUntil: "Vetado hasta {time}"
},
blockHeight: "Altura",
checkTransaction: {
description:
"Verify that funds were paid to an address by supplying the transaction ID, the recipient address, the message used for signing and the signature.\nFor a 'Spend Proof' you dont need to provide the recipient address.",
infoTitles: {
confirmations: "Confirmations",
inPool: "In pool",
validTransaction: "Valid transaction",
received: "Received amount"
},
validTransaction: {
no: "NO",
yes: "YES"
}
},
closing: "Cerrando",
connectingToBackend: "Conectando con el servicio",
contribution: "Contribution",
daemon: {
local: {
title: "Solo Servicio Local",
description:
"Seguridad completa. Se descargará la cadena de bloques entera. No podrá operar hasta que finalice el proceso de sincronización."
},
localRemote: {
title: "Servicio Local + Remoto",
description:
"Empiece a operar rápidamente gracias a esta opción predeterminada. Se descargará la cadena de bloques al completo pero se usará un nodo remoto mientras dure la descarga."
},
remote: {
title: "Solo Servicio Remoto",
description: "Menor seguridad. El monedero se conectará a un nodo remoto para realizar cualquier operación."
}
},
destinationUnknown: "Destino Desconocido",
editAddressBookEntry: "Modificar un registro de la libreta de direcciones",
loadingSettings: "Cargando configuración",
lokiBalance: "Saldo",
lokiUnlockedBalance: "Saldo libre",
lokiUnlockedShort: "Libre",
noTransactionsFound: "No se han encontrado transacciones",
notes: "Notas",
numberOfUnspentOutputs: "Número de salidas no gastadas",
paymentID: "ID de pago",
priorityOptions: {
automatic: "Automática",
slow: "Lenta",
normal: "Normal",
fast: "Rápida",
fastest: "La más rápida"
},
peerList: "Lista de pares",
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: "Leyendo el listado de monederos",
recentIncomingTransactionsToAddress: "Transacciones recientes recibidas en esta dirección",
recentTransactionsWithAddress: "Transacciones recientes con esta dirección",
rescanModalDescription: "Escoja entre un examen completo o examinar solo las salidas gastadas.",
saveSeedWarning: "¡Por favor, cópielas y guárdelas en un sitio seguro!",
saveToAddressBook: "Guardar en la libreta de direcciones",
seedWords: "Palabras semilla",
selectLanguage: "Escoja un idioma",
serviceNodeRegistrationDescription:
'Introduzca la orden {registerCommand} generada por el servicio (lokid) que se está intentado registrar como Nodo de Servicio usando la instrucción "{prepareCommand}"',
spendKey: "Clave de gasto",
startingDaemon: "Iniciando servicio",
startingWallet: "Iniciando monedero",
switchToDateSelect: "Cambiar a selección por fecha",
switchToHeightSelect: "Cambiar a selección por altura",
transactionID: "ID de la transacción",
transactionConfirmed: "confirmada",
transactions: {
amount: "Cantidad",
description: "Transacción {type}",
fee: "Comisión",
paidBySender: "pagada por el remitente",
received: "Recibida",
sent: "Enviada",
sentTo: "Transacción {type} enviada a",
timestamp: "Fecha y hora",
types: {
all: "Todas",
incoming: "Recibida",
outgoing: "Emitida",
pending: "Pendiente",
pendingIncoming: "Recibida pendiente",
pendingOutgoing: "Emitida pendiente",
miner: "Minería",
serviceNode: "Nodo de Servicio",
governance: "Gobernanza",
stake: "Retención participación",
failed: "Fallida"
}
},
unspentOutputs: "Salidas no gastadas",
userNotUsedAddress: "No ha utilizado esta dirección",
userUsedAddress: "Ha utilizado esta dirección",
viewKey: "Clave de visualización",
viewOnlyMode:
"Este monedero solo permite visualizar operaciones. Por favor, abra uno completo para poder transferir fondos."
},
titles: {
addressBook: "Libreta de direcciones",
addressDetails: "Detalles de la dirección",
advanced: {
checkTransaction: "CHECK TRANSACTION",
prove: "PROVE"
},
changePassword: "Modificar contraseña",
configure: "Configurar",
currentlyStakedNodes: "Currently staked nodes",
privateKeys: "Claves privadas",
rescanWallet: "Volver a examinar monedero",
serviceNode: {
registration: "REGISTRO",
staking: "PARTICIPACIÓN"
},
settings: {
title: "Configuración",
tabs: {
general: "General",
language: "Idioma",
peers: "Pares"
}
},
transactionDetails: "Detalles de la transacción",
transactions: "Transacciones",
wallet: {
createNew: "Crear un monedero nuevo",
createdOrRestored: "Monedero creado/restaurado",
importFromFile: "Importar monedero de un archivo",
importFromLegacyGUI: "Importar monedero de una interfaz gráfica heredada",
importFromOldGUI: "Importar monedero de una interfaz gráfica antigua",
restoreFromSeed: "Restaurar monedero mediante semilla",
restoreViewOnly: "Restaurar monedero de solo visualización"
},
welcome: "Bienvenido/a",
yourWallets: "Sus Monederos"
}
};

View File

@ -1,492 +1,505 @@
export default {
buttons: {
// All button text is uppercased in the gui
advanced: "ADVANCED",
all: "TOUT",
back: "RETOUR",
browse: "NAVIGUER",
cancel: "ANNULER",
change: "CHANGER",
check: "CHECK",
clear: "CLEAR",
close: "FERMER",
contacts: "CONTACTS",
copyAddress: "ADRESSE DE COPIE",
copySignature: "COPY SIGNATURE",
createWallet: "CRÉER UN PORTEFEUILLE",
delete: "SUPPRIMER",
edit: "MODIFIER",
export: "EXPORTER",
generate: "GENERATE",
import: "IMPORTER",
importWallet: "IMPORTER UN PORTEFEUILLE | IMPORTER DES PORTEFEUILLE",
next: "SUIVANT",
openWallet: "OUVRIR LE PORTEFEUILLE",
receive: "RECEVOIR",
registerServiceNode: "ENREGISTRER UN NOEUD DE SERVICE",
rescan: "RÉANALYSER",
restoreWallet: "RESTAURER LE PORTEFEUILLE",
save: "SAUVEGARDER",
saveTxNotes: "SAUVEGARDER LES NOTES DE TRANSACTION",
selectLocation: "SÉLECTIONNEZ L'EMPLACEMENT",
selectWalletFile: "SÉLECTIONNER LE FICHIER PORTEFEUILLE",
send: "ENVOYER",
sendCoins: "ENVOYER DES PIECES",
serviceNode: "NOEUD DE SERVICE",
settings: "RÉGLAGES",
showQRCode: "AFFICHER LE QR CODE",
showTxDetails: "AFFICHER LES DETAILS DE TRANSACTION",
stake: "STAKE",
sweepAll: "TOUT BALAYER",
unlock: "UNLOCK",
viewOnExplorer: "VUE SUR LEXPLORATEUR"
},
dialog: {
// Generic buttons
buttons: {
// All button text is uppercased in the gui
advanced: "ADVANCED",
all: "TOUT",
back: "RETOUR",
browse: "NAVIGUER",
cancel: "ANNULER",
change: "CHANGER",
check: "CHECK",
clear: "CLEAR",
close: "FERMER",
contacts: "CONTACTS",
copyAddress: "ADRESSE DE COPIE",
copySignature: "COPY SIGNATURE",
createWallet: "CRÉER UN PORTEFEUILLE",
delete: "SUPPRIMER",
edit: "MODIFIER",
export: "EXPORTER",
generate: "GENERATE",
import: "IMPORTER",
importWallet: "IMPORTER UN PORTEFEUILLE | IMPORTER DES PORTEFEUILLE",
next: "SUIVANT",
openWallet: "OUVRIR LE PORTEFEUILLE",
receive: "RECEVOIR",
registerServiceNode: "ENREGISTRER UN NOEUD DE SERVICE",
rescan: "RÉANALYSER",
restoreWallet: "RESTAURER LE PORTEFEUILLE",
save: "SAUVEGARDER",
saveTxNotes: "SAUVEGARDER LES NOTES DE TRANSACTION",
selectLocation: "SÉLECTIONNEZ L'EMPLACEMENT",
selectWalletFile: "SÉLECTIONNER LE FICHIER PORTEFEUILLE",
send: "ENVOYER",
sendCoins: "ENVOYER DES PIECES",
serviceNode: "NOEUD DE SERVICE",
settings: "RÉGLAGES",
showQRCode: "AFFICHER LE QR CODE",
showTxDetails: "AFFICHER LES DETAILS DE TRANSACTION",
stake: "STAKE",
sweepAll: "TOUT BALAYER",
unlock: "UNLOCK",
viewOnExplorer: "VUE SUR LEXPLORATEUR"
ok: "OK",
cancel: "ANNULER",
open: "OUVRIR"
},
dialog: {
// Generic buttons
buttons: {
ok: "OK",
cancel: "ANNULER",
open: "OUVRIR"
},
// Dialogs
banPeer: {
title: "Pair exclu",
peerDetailsTitle: "Détails du pair",
message: "Entrez le temps en secondes pour exclure un pair.\nDefaut 3600 = 1 heure.",
ok: "Pair exclu"
},
copyAddress: {
title: "Adresse de copie",
message: "Un identifiant de paiement est associé à cette adresse.\nAssurez-vous de copier l'identifiant de paiement séparément."
},
copyPrivateKeys: {
// Copy {seedWords/viewKey/spendKey}
title: "Copier {type}",
message: "Faites attention à qui vous envoyez vos clés privées, car ils peuvent contrôler vos fonds.",
seedWords: "mots-clés",
viewKey: "Clé daffichage",
spendKey: "Clé de dépenses"
},
deleteWallet: {
title: "Supprimer le portefeuille",
message: "Êtes-vous absolument certain de vouloir supprimer votre portefeuille?\nAssurez-vous que vos clés privées sont sauvegardées. \nCE PROCESSUS N'EST PAS REVERSIBLE !",
ok: "Supprimer"
},
exit: {
title: "Quitter",
message: "Êtes-vous sûr de vouloir quitter?",
ok: "Quitter"
},
keyImages: {
title: "{type} images clés",
message: "Souhaitez-vous {type} les images clés ?",
export: "Exporter",
import: "Importer"
},
noPassword: {
title: "Aucun mot de passe défini",
message: "Êtes-vous sûr de vouloir créer un portefeuille sans mot de passe ?",
ok: "OUI"
},
password: {
title: "Mot de passe",
message: "Entrez le mot de passe du portefeuille pour continuer."
},
registerServiceNode: {
title: "Enregistrer le nœud de service",
message: "Voulez-vous enregistrer le nœud de service ?",
ok: "ENREGISTRER"
},
rescan: {
title: "Réanalyser le portefeuille",
message: "Avertissement: Quelques informations sur les transactions précédentes \nTelles que l'adresse du destinataire seront perdues.",
ok: "RÉANALYSER"
},
restart: {
title: "REDÉMARRER",
message: "Les modifications nécessitent un redémarrage. Voulez-vous redémarrer maintenant ?",
ok: "REDÉMARRER"
},
showPrivateKeys: {
title: "AFFICHER LES CLÉS PRIVÉES",
message: "Voulez-vous afficher vos clés privées ?",
ok: "AFFICHER"
},
stake: {
title: "Stake",
message: "Voulez-vous miser ?",
ok: "STAKE"
},
sweepAll: {
title: "Tout balayer",
message: "Voulez-vous tout balayer?",
ok: "TOUT BALAYER"
},
sweepAllWarning: {
title: "Sweep all warning",
message: "You are about to combine all of your unspent funds by sending a transaction to yourself, your wallet may show a balance of 0 temporarily, after 10 blocks your funds will unlock and you may stake normally.",
ok: "CONTINUE"
},
switchWallet: {
title: "CHANGER DE PORTEFEUILLE",
closeMessage: "Êtes-vous sûr de vouloir fermer le portefeuille actuel ?",
restartMessage: "Le portefeuille RPC est en cours de synchronisation. \nSi vous souhaitez changer de portefeuille, vous devez redémarrer l'application. \nVous allez perdre votre progression concernant la synchronisation, vous devrez à nouveau analyser la blockchain."
},
transactionDetails: {
title: "Détails de la transaction",
ok: "FERMER"
},
transfer: {
title: "Transfert",
message: "Voulez-vous envoyer la transaction ?",
ok: "ENVOYER"
},
unlockConfirm: {
title: "Confirmer le déverrouillage",
ok: "DÉVERROUILLER"
},
unlockServiceNode: {
title: "Déverrouiller le nœud de service",
confirmTitle: "Confirmer le déverrouillage",
message: "Voulez-vous déverrouiller le nœud de service ?",
ok: "DÉVERROUILLER"
},
unlockServiceNodeWarning: {
title: "Unlock service node warning",
message: "Unlocking a partial stake in a node will also unstake for any other participants, if staking in a shared node its best to let the operator and other participants know you are unstaking.",
ok: "CONTINUE"
}
// Dialogs
banPeer: {
title: "Pair exclu",
peerDetailsTitle: "Détails du pair",
message: "Entrez le temps en secondes pour exclure un pair.\nDefaut 3600 = 1 heure.",
ok: "Pair exclu"
},
fieldLabels: {
// Field labels are also all uppercased
address: "ADRESSE",
amount: "MONTANT",
awardRecepientAddress: "ADRESSE DU BÉNÉFICIAIRE POUR LA RÉCOMPENSE",
confirmPassword: "CONFIRMER LE MOT DE PASSE",
daemonLogLevel: "NIVEAU D'IMPORTANCE DU DÉMON",
daemonP2pPort: "PORT P2P DU DÉMON",
daemonZMQPort: "PORT ZMQ DU DÉMON",
dataStoragePath: "CHEMIN DE STOCKAGE DE DONNÉES",
filter: "FILTER",
filterTransactionType: "FILTRER PAR TYPE DE TRANSACTION",
internalWalletPort: "PORT DE PORTEFEUILLE INTERNE",
keyImages: {
exportDirectory: "RÉPERTOIRE D'EXPORTATION D'IMAGES CLÉS",
importFile: "FICHIER D'IMPORTATION D'IMAGES CLÉS"
},
limitDownloadRate: "TAUX LIMITE DE TÉLÉCHARGEMENT",
limitUploadRate: "TAUX LIMITE DUPLOAD",
localDaemonIP: "IP DU DÉMON LOCAL",
localDaemonPort: "PORT DU DÉMON LOCAL",
maxIncomingPeers: "MAXIMUM DE PAIRS ENTRANTS",
maxOutgoingPeers: "NOMBRE MAXIMUM DE PAIRS SORTANTS",
message: "MESSAGE",
mnemonicSeed: "MOT MNÉMONIQUE",
name: "NOM",
newWalletName: "NOUVEAU NOM DU PORTEFEUILLE",
notes: "NOTES",
optional: "OPTIONNEL",
password: "MOT DE PASSE",
paymentId: "ID DE PAIEMENT",
priority: "PRIORITÉ",
remoteNodeHost: "HÔTE À DISTANCE DE NOEUD",
remoteNodePort: "PORT DE NOEUD DISTANT",
restoreFromBlockHeight: "RESTAURATION DE LA HAUTEUR DU BLOC",
restoreFromDate: "RESTAURATION A PARTIR DE LA DATE",
seedLanguage: "LANGAGE SEED",
serviceNodeCommand: "COMMANDE DE NŒUD DE SERVICE",
serviceNodeKey: "CLÉ DE SERVICE NODE",
signature: "SIGNATURE",
transactionId: "TRANSACTION ID",
walletFile: "DOSSIER DU PORTEFEUILLE",
walletLogLevel: "NIVEAU D'IMPORTANCE DU PORTEFEUILLE",
walletName: "NOM DU PORTEFEUILLE",
walletRPCPort: "PORT RPC DU PORTEFEUILLE",
walletStoragePath: "CHEMIN DE STOCKAGE DU PORTEFEUILLE",
// These are specific labels which do not get uppercased
confirmNewPassword: "Confirmez le nouveau mot de passe",
newPassword: "Nouveau mot de passe",
oldPassword: "Ancien mot de passe",
rescanFullBlockchain: "Scanner de nouveau la blockchain complète",
rescanSpentOutputs: "Nouvelle analyse des sorties dépensées",
transactionNotes: "Notes de transaction",
chooseNetwork: "Choisir un réseau",
network: "Réseau"
copyAddress: {
title: "Adresse de copie",
message:
"Un identifiant de paiement est associé à cette adresse.\nAssurez-vous de copier l'identifiant de paiement séparément."
},
footer: {
ready: "PRÊT",
scanning: "ANALYSE",
status: "Statut",
syncing: "SYNCHRONISATION",
remote: "Nœuds éloignés",
wallet: "Portefeuille"
copyPrivateKeys: {
// Copy {seedWords/viewKey/spendKey}
title: "Copier {type}",
message: "Faites attention à qui vous envoyez vos clés privées, car ils peuvent contrôler vos fonds.",
seedWords: "mots-clés",
viewKey: "Clé daffichage",
spendKey: "Clé de dépenses"
},
menuItems: {
about: "A propos",
changePassword: "Changer le mot de passe",
copyAddress: "Copier ladresse",
copyQR: "Copier le QR code",
copySeedWords: "Copier les mots clés",
copySpendKey: "Copier la clé de dépense",
copyServiceNodeKey: "Copy service node key",
copyTransactionId: "Copier l'ID de transaction",
copyViewKey: "Copier la clé de visibilité",
createNewWallet: "Créer un nouveau portefeuille",
deleteWallet: "Supprimer le portefeuille",
exit: "Quitter le portefeuille Loki GUI",
importOldGUIWallet: "Importer le portefeuille depuis lancien GUI",
manageKeyImages: "Gérer les images clés",
openWallet: "Ouvrir le portefeuille",
rescanWallet: "Nouvelle analyse du portefeuille",
restoreWalletFile: "Restaurer le portefeuille à partir d'un fichier",
restoreWalletSeed: "Restaurer le portefeuille depuis les mots clés(SEED)",
saveQR: "Enregistrer le QR code dans un fichier",
sendToThisAddress: "Envoyer à cette adresse",
settings: "Réglages",
showDetails: "Afficher les détails",
showPrivateKeys: "Afficher les clés privées",
showQRCode: "Afficher le QR code",
switchWallet: "Changer de portefeuille",
viewOnExplorer: "Voir sur l'explorateur"
deleteWallet: {
title: "Supprimer le portefeuille",
message:
"Êtes-vous absolument certain de vouloir supprimer votre portefeuille?\nAssurez-vous que vos clés privées sont sauvegardées. \nCE PROCESSUS N'EST PAS REVERSIBLE !",
ok: "Supprimer"
},
notification: {
positive: {
addressCopied: "Adresse copiée dans le presse-papier",
bannedPeer: "Banni {host} jusqu'au {time}",
copied: "{item}copié dans le presse-papier",
itemSaved: "{item}enregistré dans {filename}",
keyImages: {
exported: "Images clés exportées vers {filename}",
imported: "Images clés importées"
},
passwordUpdated: "Mot de passe mis à jour",
qrCopied: "QR code copié dans le presse-papier",
registerServiceNodeSuccess: "Nœud de service enregistré avec succès",
sendSuccess: "Transaction envoyée avec succès",
signatureCopied: "Signature copied to clipboard",
stakeSuccess: "Mise placée avec succès",
transactionNotesSaved: "Notes de transaction enregistrées"
},
errors: {
banningPeer: "Erreur d'exclusion d'un pair",
cannotAccessRemoteNode: "Impossible d'accéder au nœud distant, veuillez essayer un autre nœud distant",
changingPassword: "Erreur de changement de mot de passe",
copyWalletFail: "Echec de la copie du portefeuille",
copyingPrivateKeys: "Erreur de la copie des clés privées",
dataPathNotFound: "Chemin de stockage des données introuvable",
differentNetType: "Le nœud distant utilise un 'nettoype' différent",
enterSeedWords: "Entrez les mots clés",
enterTransactionId: "Enter transaction ID",
enterTransactionProof: "Enter transaction proof",
enterWalletName: "Entrez un nom de portefeuille",
errorSavingItem: "Erreur de sauvegarde {item}",
failedServiceNodeUnlock: "Erreur de déverrouillage du nœud de service",
failedToSetLanguage: "Impossible de définir la langue : {lang}",
failedWalletImport: "Echec d'import du portefeuille",
failedWalletOpen: "Echec de l'ouverture du portefeuille : veuillez essayer de nouveau.",
internalError: "Erreur interne",
invalidAddress: "Adresse non valide",
invalidAmount: "Montant non valide",
invalidOldPassword: "Ancien mot de passe non valide",
invalidPassword: "Mot de passe non valide",
invalidPaymentId: "ID de paiement non valide",
invalidPrivateViewKey: "Clé de visibilité privée non valide",
invalidPublicAddress: "Adresse publique non valide",
invalidRestoreDate: "Date de restauration non valide",
invalidRestoreHeight: "Hauteur de restauration non valide",
invalidSeedLength: "Longueur de mot clé non valide",
invalidServiceNodeCommand: "Veuillez entrer la commande d'inscription d'un nœud de service",
invalidServiceNodeKey: "Clé du nœud de service non valide",
invalidWalletPath: "Chemin du portefeuille non valide",
keyImages: {
exporting: "Erreur de l'exportation des clés images",
reading: "Erreur de lecture des clés images",
importing: "Erreur d'importation des clés images"
},
negativeAmount: "Le montant ne peut être négatif",
newPasswordNoMatch: "Les nouveaux mots de passe ne correspondent pas",
newPasswordSame: "Le nouveau mot de passe doit être différent",
notEnoughBalance: "Pas assez de solde débloqué",
passwordNoMatch: "Les mots de passe ne correspondent pas",
remoteCannotBeReached: "La démon distant ne peut pas être atteint",
selectWalletFile: "Select a wallet file",
unknownError: "Une erreur inconnue s'est produite",
walletAlreadyExists: "Un portefeuille avec ce nom existe déjà",
walletPathNotFound: "Chemin de stockage des données du portefeuille introuvable",
zeroAmount: "Le montant doit être supérieur à zéro"
},
warnings: {
noKeyImageExport: "Aucune clé image n'a été trouvé pour l'export",
usingLocalNode: "Impossible d'accéder au nœud distant, basculement en local uniquement",
usingRemoteNode: "lokid introuvable, utilisation du nœud distant"
}
exit: {
title: "Quitter",
message: "Êtes-vous sûr de vouloir quitter?",
ok: "Quitter"
},
placeholders: {
additionalNotes: "Notes supplémentaires",
addressBookName: "Nom rattaché à cette adresse",
filterTx: "Enter an ID, name, address or amount",
hexCharacters: "{count} caractères hexadécimaux",
mnemonicSeed: "25 (ou 24) mot mnémonique",
pasteTransactionId: "Paste transaction ID",
pasteTransactionProof: "Paste transaction proof",
proveOptionalMessage: "Optional message against which the signature is signed",
recipientWalletAddress: "Recipient's wallet address",
selectAFile: "Veuillez sélectionner un fichier",
transactionNotes: "Notes additionnelles attachées à la transaction",
walletName: "Un nom pour votre portefeuille",
walletPassword: "Un mot de passe optionnel pour votre portefeuille"
keyImages: {
title: "{type} images clés",
message: "Souhaitez-vous {type} les images clés ?",
export: "Exporter",
import: "Importer"
},
strings: {
addAddressBookEntry: "Ajoutez une entrée de carnet d'adresses",
addressBookIsEmpty: "Le carnet d'adresses est vide",
addressBookDetails: "Détails du carnet dadresse",
addresses: {
myPrimaryAddress: "Mon adresse principale",
myUnusedAddresses: "Mes adresses non utilisées",
myUsedAddresses: "Mes adresses utilisées",
notYourAddress: "Ce n'est pas votre adresse !",
primaryAddress: "Adresse principale",
subAddress: "Sous adresse",
subAddressIndex: "Index {index}"
},
advancedOptions: "Options avancées",
bannedPeers: {
title: "Les pairs exclus (les exclusions seront effacées si le portefeuille est redémarré)",
bannedUntil: "Exclu jusqu'au {time}"
},
blockHeight: "Hauteur",
checkTransaction: {
description: "Verify that funds were paid to an address by supplying the transaction ID, the recipient address, the message used for signing and the signature.\nFor a 'Spend Proof' you dont need to provide the recipient address.",
infoTitles: {
confirmations: "Confirmations",
inPool: "In pool",
validTransaction: "Valid transaction",
received: "Received amount"
},
validTransaction: {
no: "NO",
yes: "YES"
}
},
closing: "Fermeture",
connectingToBackend: "Connexion à l'arrière-plan",
contribution: "Contribution",
daemon: {
local: {
title: "Démon local seulement",
description: "Sécurité totale, le portefeuille téléchargera la chaîne de blocs entière. Vous ne pourrez pas faire de transactions jusqu'à ce que la synchronisation soit complète."
},
localRemote: {
title: "Local + démon distant",
description: "Vous pourrez utiliser rapidement le portefeuille avec cette option par défaut. Le portefeuille téléchargera la chaîne de blocs entière, mais utilise un nœud distant pendant la synchronisation."
},
remote: {
title: "Démon distant seulement",
description: "Moins de sécurité, le portefeuille se connectera à un noeud distant pour faire toutes les transactions."
}
},
destinationUnknown: "Destination inconnue",
editAddressBookEntry: "Modifiez l'entrée du carnet d'adresses",
loadingSettings: "Chargement des réglages",
lokiBalance: "Solde",
lokiUnlockedBalance: "Solde débloqué",
lokiUnlockedShort: "Débloqué",
noTransactionsFound: "Aucune transaction trouvée",
notes: "Notes",
numberOfUnspentOutputs: "Nombre de sorties non dépensées",
paymentID: "ID de paiement",
peerList: "Liste des pairs",
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.",
priorityOptions: {
automatic: "Automatique",
slow: "Lent",
normal: "Normal",
fast: "Rapide",
fastest: "Le plus rapide"
},
readingWalletList: "Liste des portefeuilles lisibles",
recentIncomingTransactionsToAddress: "Transactions récentes entrantes vers cette adresse",
recentTransactionsWithAddress: "Transactions récentes avec cette adresse",
rescanModalDescription: "Sélectionnez une nouvelle analyse complète ou une nouvelle analyse des sorties dépensées seulement.",
saveSeedWarning: "Veuillez copier et enregistrer cela dans un lieu sécurisé !",
saveToAddressBook: "Enregistrez dans le carnet d'adresses",
seedWords: "Mots clés",
selectLanguage: "Sélectionnez une langue",
serviceNodeRegistrationDescription: "Entrez la commande {registerCommand} produite par le démon qui est enregistrée pour devenir un noeud de service en utilisant la commande \"{prepareCommand}\"",
spendKey: "Clé dépensée",
startingDaemon: "Démarrage du démon",
startingWallet: "Démarrage du portefeuille",
switchToDateSelect: "Basculer vers la sélection par date",
switchToHeightSelect: "Basculer vers la sélection par hauteur",
transactionID: "ID de transaction",
transactionConfirmed: "confirmée",
transactions: {
amount: "Montant",
description: "Transaction {type}",
fee: "Frais",
paidBySender: "Payé par l'expéditeur",
received: "Reçu",
sent: "Envoyé",
sentTo: "Transaction {type} envoyée à",
timestamp: "Horodatage",
types: {
all: "Toutes",
incoming: "Entrantes",
outgoing: "Sortantes",
pending: "En attente",
pendingIncoming: "Entrantes en attente",
pendingOutgoing: "Sortantes en attente",
miner: "Miner",
serviceNode: "Noeud de service",
governance: "Gouvernance",
stake: "Stake",
failed: "Echouées"
}
},
unspentOutputs: "Sorties non dépensées",
userNotUsedAddress: "Vous n'avez pas utilisé cette adresse",
userUsedAddress: "Vous avez utilisé cette adresse",
viewKey: "Clé de visibilité",
viewOnlyMode: "Mode d'affichage seulement. Veuillez charger le portefeuille complet pour envoyer des pièces."
noPassword: {
title: "Aucun mot de passe défini",
message: "Êtes-vous sûr de vouloir créer un portefeuille sans mot de passe ?",
ok: "OUI"
},
titles: {
addressBook: "Carnet d'adresses",
addressDetails: "Détails de l'adresse",
advanced: {
checkTransaction: "CHECK TRANSACTION",
prove: "PROVE"
},
changePassword: "Changer de mot de passe",
configure: "Configurer",
currentlyStakedNodes: "Currently staked nodes",
privateKeys: "Clés privées",
rescanWallet: "Analysez de nouveau le portefeuille",
serviceNode: {
registration: "ENREGISTREMENT",
staking: "STAKING"
},
settings: {
title: "Réglages",
tabs: {
general: "Général",
language: "Langue",
peers: "Pairs"
}
},
transactionDetails: "Détails de la transaction",
transactions: "Transactions",
wallet: {
createNew: "Créer un nouveau portefeuille",
createdOrRestored: "Créer ou restaurer un portefeuille",
importFromFile: "Importer un portefeuille à partir d'un fichier",
importFromLegacyGUI: "Importer un portefeuille à partir d'un héritage GUI",
importFromOldGUI: "Importer un portefeuille à partir d'un ancien GUI",
restoreFromSeed: "Restaurer un portefeuille à partir de mots clés",
restoreViewOnly: "Restaurer un portefeuille en mode d'affichage seulement"
},
welcome: "Bienvenue",
yourWallets: "Vos portefeuilles"
password: {
title: "Mot de passe",
message: "Entrez le mot de passe du portefeuille pour continuer."
},
registerServiceNode: {
title: "Enregistrer le nœud de service",
message: "Voulez-vous enregistrer le nœud de service ?",
ok: "ENREGISTRER"
},
rescan: {
title: "Réanalyser le portefeuille",
message:
"Avertissement: Quelques informations sur les transactions précédentes \nTelles que l'adresse du destinataire seront perdues.",
ok: "RÉANALYSER"
},
restart: {
title: "REDÉMARRER",
message: "Les modifications nécessitent un redémarrage. Voulez-vous redémarrer maintenant ?",
ok: "REDÉMARRER"
},
showPrivateKeys: {
title: "AFFICHER LES CLÉS PRIVÉES",
message: "Voulez-vous afficher vos clés privées ?",
ok: "AFFICHER"
},
stake: {
title: "Stake",
message: "Voulez-vous miser ?",
ok: "STAKE"
},
sweepAll: {
title: "Tout balayer",
message: "Voulez-vous tout balayer?",
ok: "TOUT BALAYER"
},
sweepAllWarning: {
title: "Sweep all warning",
message:
"You are about to combine all of your unspent funds by sending a transaction to yourself, your wallet may show a balance of 0 temporarily, after 10 blocks your funds will unlock and you may stake normally.",
ok: "CONTINUE"
},
switchWallet: {
title: "CHANGER DE PORTEFEUILLE",
closeMessage: "Êtes-vous sûr de vouloir fermer le portefeuille actuel ?",
restartMessage:
"Le portefeuille RPC est en cours de synchronisation. \nSi vous souhaitez changer de portefeuille, vous devez redémarrer l'application. \nVous allez perdre votre progression concernant la synchronisation, vous devrez à nouveau analyser la blockchain."
},
transactionDetails: {
title: "Détails de la transaction",
ok: "FERMER"
},
transfer: {
title: "Transfert",
message: "Voulez-vous envoyer la transaction ?",
ok: "ENVOYER"
},
unlockConfirm: {
title: "Confirmer le déverrouillage",
ok: "DÉVERROUILLER"
},
unlockServiceNode: {
title: "Déverrouiller le nœud de service",
confirmTitle: "Confirmer le déverrouillage",
message: "Voulez-vous déverrouiller le nœud de service ?",
ok: "DÉVERROUILLER"
},
unlockServiceNodeWarning: {
title: "Unlock service node warning",
message:
"Unlocking a partial stake in a node will also unstake for any other participants, if staking in a shared node its best to let the operator and other participants know you are unstaking.",
ok: "CONTINUE"
}
}
},
fieldLabels: {
// Field labels are also all uppercased
address: "ADRESSE",
amount: "MONTANT",
awardRecepientAddress: "ADRESSE DU BÉNÉFICIAIRE POUR LA RÉCOMPENSE",
confirmPassword: "CONFIRMER LE MOT DE PASSE",
daemonLogLevel: "NIVEAU D'IMPORTANCE DU DÉMON",
daemonP2pPort: "PORT P2P DU DÉMON",
daemonZMQPort: "PORT ZMQ DU DÉMON",
dataStoragePath: "CHEMIN DE STOCKAGE DE DONNÉES",
filter: "FILTER",
filterTransactionType: "FILTRER PAR TYPE DE TRANSACTION",
internalWalletPort: "PORT DE PORTEFEUILLE INTERNE",
keyImages: {
exportDirectory: "RÉPERTOIRE D'EXPORTATION D'IMAGES CLÉS",
importFile: "FICHIER D'IMPORTATION D'IMAGES CLÉS"
},
limitDownloadRate: "TAUX LIMITE DE TÉLÉCHARGEMENT",
limitUploadRate: "TAUX LIMITE DUPLOAD",
localDaemonIP: "IP DU DÉMON LOCAL",
localDaemonPort: "PORT DU DÉMON LOCAL",
maxIncomingPeers: "MAXIMUM DE PAIRS ENTRANTS",
maxOutgoingPeers: "NOMBRE MAXIMUM DE PAIRS SORTANTS",
message: "MESSAGE",
mnemonicSeed: "MOT MNÉMONIQUE",
name: "NOM",
newWalletName: "NOUVEAU NOM DU PORTEFEUILLE",
notes: "NOTES",
optional: "OPTIONNEL",
password: "MOT DE PASSE",
paymentId: "ID DE PAIEMENT",
priority: "PRIORITÉ",
remoteNodeHost: "HÔTE À DISTANCE DE NOEUD",
remoteNodePort: "PORT DE NOEUD DISTANT",
restoreFromBlockHeight: "RESTAURATION DE LA HAUTEUR DU BLOC",
restoreFromDate: "RESTAURATION A PARTIR DE LA DATE",
seedLanguage: "LANGAGE SEED",
serviceNodeCommand: "COMMANDE DE NŒUD DE SERVICE",
serviceNodeKey: "CLÉ DE SERVICE NODE",
signature: "SIGNATURE",
transactionId: "TRANSACTION ID",
walletFile: "DOSSIER DU PORTEFEUILLE",
walletLogLevel: "NIVEAU D'IMPORTANCE DU PORTEFEUILLE",
walletName: "NOM DU PORTEFEUILLE",
walletRPCPort: "PORT RPC DU PORTEFEUILLE",
walletStoragePath: "CHEMIN DE STOCKAGE DU PORTEFEUILLE",
// These are specific labels which do not get uppercased
confirmNewPassword: "Confirmez le nouveau mot de passe",
newPassword: "Nouveau mot de passe",
oldPassword: "Ancien mot de passe",
rescanFullBlockchain: "Scanner de nouveau la blockchain complète",
rescanSpentOutputs: "Nouvelle analyse des sorties dépensées",
transactionNotes: "Notes de transaction",
chooseNetwork: "Choisir un réseau",
network: "Réseau"
},
footer: {
ready: "PRÊT",
scanning: "ANALYSE",
status: "Statut",
syncing: "SYNCHRONISATION",
remote: "Nœuds éloignés",
wallet: "Portefeuille"
},
menuItems: {
about: "A propos",
changePassword: "Changer le mot de passe",
copyAddress: "Copier ladresse",
copyQR: "Copier le QR code",
copySeedWords: "Copier les mots clés",
copySpendKey: "Copier la clé de dépense",
copyServiceNodeKey: "Copy service node key",
copyTransactionId: "Copier l'ID de transaction",
copyViewKey: "Copier la clé de visibilité",
createNewWallet: "Créer un nouveau portefeuille",
deleteWallet: "Supprimer le portefeuille",
exit: "Quitter le portefeuille Loki GUI",
importOldGUIWallet: "Importer le portefeuille depuis lancien GUI",
manageKeyImages: "Gérer les images clés",
openWallet: "Ouvrir le portefeuille",
rescanWallet: "Nouvelle analyse du portefeuille",
restoreWalletFile: "Restaurer le portefeuille à partir d'un fichier",
restoreWalletSeed: "Restaurer le portefeuille depuis les mots clés(SEED)",
saveQR: "Enregistrer le QR code dans un fichier",
sendToThisAddress: "Envoyer à cette adresse",
settings: "Réglages",
showDetails: "Afficher les détails",
showPrivateKeys: "Afficher les clés privées",
showQRCode: "Afficher le QR code",
switchWallet: "Changer de portefeuille",
viewOnExplorer: "Voir sur l'explorateur"
},
notification: {
positive: {
addressCopied: "Adresse copiée dans le presse-papier",
bannedPeer: "Banni {host} jusqu'au {time}",
copied: "{item}copié dans le presse-papier",
itemSaved: "{item}enregistré dans {filename}",
keyImages: {
exported: "Images clés exportées vers {filename}",
imported: "Images clés importées"
},
passwordUpdated: "Mot de passe mis à jour",
qrCopied: "QR code copié dans le presse-papier",
registerServiceNodeSuccess: "Nœud de service enregistré avec succès",
sendSuccess: "Transaction envoyée avec succès",
signatureCopied: "Signature copied to clipboard",
stakeSuccess: "Mise placée avec succès",
transactionNotesSaved: "Notes de transaction enregistrées"
},
errors: {
banningPeer: "Erreur d'exclusion d'un pair",
cannotAccessRemoteNode: "Impossible d'accéder au nœud distant, veuillez essayer un autre nœud distant",
changingPassword: "Erreur de changement de mot de passe",
copyWalletFail: "Echec de la copie du portefeuille",
copyingPrivateKeys: "Erreur de la copie des clés privées",
dataPathNotFound: "Chemin de stockage des données introuvable",
differentNetType: "Le nœud distant utilise un 'nettoype' différent",
enterSeedWords: "Entrez les mots clés",
enterTransactionId: "Enter transaction ID",
enterTransactionProof: "Enter transaction proof",
enterWalletName: "Entrez un nom de portefeuille",
errorSavingItem: "Erreur de sauvegarde {item}",
failedServiceNodeUnlock: "Erreur de déverrouillage du nœud de service",
failedToSetLanguage: "Impossible de définir la langue : {lang}",
failedWalletImport: "Echec d'import du portefeuille",
failedWalletOpen: "Echec de l'ouverture du portefeuille : veuillez essayer de nouveau.",
internalError: "Erreur interne",
invalidAddress: "Adresse non valide",
invalidAmount: "Montant non valide",
invalidOldPassword: "Ancien mot de passe non valide",
invalidPassword: "Mot de passe non valide",
invalidPaymentId: "ID de paiement non valide",
invalidPrivateViewKey: "Clé de visibilité privée non valide",
invalidPublicAddress: "Adresse publique non valide",
invalidRestoreDate: "Date de restauration non valide",
invalidRestoreHeight: "Hauteur de restauration non valide",
invalidSeedLength: "Longueur de mot clé non valide",
invalidServiceNodeCommand: "Veuillez entrer la commande d'inscription d'un nœud de service",
invalidServiceNodeKey: "Clé du nœud de service non valide",
invalidWalletPath: "Chemin du portefeuille non valide",
keyImages: {
exporting: "Erreur de l'exportation des clés images",
reading: "Erreur de lecture des clés images",
importing: "Erreur d'importation des clés images"
},
negativeAmount: "Le montant ne peut être négatif",
newPasswordNoMatch: "Les nouveaux mots de passe ne correspondent pas",
newPasswordSame: "Le nouveau mot de passe doit être différent",
notEnoughBalance: "Pas assez de solde débloqué",
passwordNoMatch: "Les mots de passe ne correspondent pas",
remoteCannotBeReached: "La démon distant ne peut pas être atteint",
selectWalletFile: "Select a wallet file",
unknownError: "Une erreur inconnue s'est produite",
walletAlreadyExists: "Un portefeuille avec ce nom existe déjà",
walletPathNotFound: "Chemin de stockage des données du portefeuille introuvable",
zeroAmount: "Le montant doit être supérieur à zéro"
},
warnings: {
noKeyImageExport: "Aucune clé image n'a été trouvé pour l'export",
usingLocalNode: "Impossible d'accéder au nœud distant, basculement en local uniquement",
usingRemoteNode: "lokid introuvable, utilisation du nœud distant"
}
},
placeholders: {
additionalNotes: "Notes supplémentaires",
addressBookName: "Nom rattaché à cette adresse",
filterTx: "Enter an ID, name, address or amount",
hexCharacters: "{count} caractères hexadécimaux",
mnemonicSeed: "25 (ou 24) mot mnémonique",
pasteTransactionId: "Paste transaction ID",
pasteTransactionProof: "Paste transaction proof",
proveOptionalMessage: "Optional message against which the signature is signed",
recipientWalletAddress: "Recipient's wallet address",
selectAFile: "Veuillez sélectionner un fichier",
transactionNotes: "Notes additionnelles attachées à la transaction",
walletName: "Un nom pour votre portefeuille",
walletPassword: "Un mot de passe optionnel pour votre portefeuille"
},
strings: {
addAddressBookEntry: "Ajoutez une entrée de carnet d'adresses",
addressBookIsEmpty: "Le carnet d'adresses est vide",
addressBookDetails: "Détails du carnet dadresse",
addresses: {
myPrimaryAddress: "Mon adresse principale",
myUnusedAddresses: "Mes adresses non utilisées",
myUsedAddresses: "Mes adresses utilisées",
notYourAddress: "Ce n'est pas votre adresse !",
primaryAddress: "Adresse principale",
subAddress: "Sous adresse",
subAddressIndex: "Index {index}"
},
advancedOptions: "Options avancées",
bannedPeers: {
title: "Les pairs exclus (les exclusions seront effacées si le portefeuille est redémarré)",
bannedUntil: "Exclu jusqu'au {time}"
},
blockHeight: "Hauteur",
checkTransaction: {
description:
"Verify that funds were paid to an address by supplying the transaction ID, the recipient address, the message used for signing and the signature.\nFor a 'Spend Proof' you dont need to provide the recipient address.",
infoTitles: {
confirmations: "Confirmations",
inPool: "In pool",
validTransaction: "Valid transaction",
received: "Received amount"
},
validTransaction: {
no: "NO",
yes: "YES"
}
},
closing: "Fermeture",
connectingToBackend: "Connexion à l'arrière-plan",
contribution: "Contribution",
daemon: {
local: {
title: "Démon local seulement",
description:
"Sécurité totale, le portefeuille téléchargera la chaîne de blocs entière. Vous ne pourrez pas faire de transactions jusqu'à ce que la synchronisation soit complète."
},
localRemote: {
title: "Local + démon distant",
description:
"Vous pourrez utiliser rapidement le portefeuille avec cette option par défaut. Le portefeuille téléchargera la chaîne de blocs entière, mais utilise un nœud distant pendant la synchronisation."
},
remote: {
title: "Démon distant seulement",
description:
"Moins de sécurité, le portefeuille se connectera à un noeud distant pour faire toutes les transactions."
}
},
destinationUnknown: "Destination inconnue",
editAddressBookEntry: "Modifiez l'entrée du carnet d'adresses",
loadingSettings: "Chargement des réglages",
lokiBalance: "Solde",
lokiUnlockedBalance: "Solde débloqué",
lokiUnlockedShort: "Débloqué",
noTransactionsFound: "Aucune transaction trouvée",
notes: "Notes",
numberOfUnspentOutputs: "Nombre de sorties non dépensées",
paymentID: "ID de paiement",
peerList: "Liste des pairs",
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.",
priorityOptions: {
automatic: "Automatique",
slow: "Lent",
normal: "Normal",
fast: "Rapide",
fastest: "Le plus rapide"
},
readingWalletList: "Liste des portefeuilles lisibles",
recentIncomingTransactionsToAddress: "Transactions récentes entrantes vers cette adresse",
recentTransactionsWithAddress: "Transactions récentes avec cette adresse",
rescanModalDescription:
"Sélectionnez une nouvelle analyse complète ou une nouvelle analyse des sorties dépensées seulement.",
saveSeedWarning: "Veuillez copier et enregistrer cela dans un lieu sécurisé !",
saveToAddressBook: "Enregistrez dans le carnet d'adresses",
seedWords: "Mots clés",
selectLanguage: "Sélectionnez une langue",
serviceNodeRegistrationDescription:
'Entrez la commande {registerCommand} produite par le démon qui est enregistrée pour devenir un noeud de service en utilisant la commande "{prepareCommand}"',
spendKey: "Clé dépensée",
startingDaemon: "Démarrage du démon",
startingWallet: "Démarrage du portefeuille",
switchToDateSelect: "Basculer vers la sélection par date",
switchToHeightSelect: "Basculer vers la sélection par hauteur",
transactionID: "ID de transaction",
transactionConfirmed: "confirmée",
transactions: {
amount: "Montant",
description: "Transaction {type}",
fee: "Frais",
paidBySender: "Payé par l'expéditeur",
received: "Reçu",
sent: "Envoyé",
sentTo: "Transaction {type} envoyée à",
timestamp: "Horodatage",
types: {
all: "Toutes",
incoming: "Entrantes",
outgoing: "Sortantes",
pending: "En attente",
pendingIncoming: "Entrantes en attente",
pendingOutgoing: "Sortantes en attente",
miner: "Miner",
serviceNode: "Noeud de service",
governance: "Gouvernance",
stake: "Stake",
failed: "Echouées"
}
},
unspentOutputs: "Sorties non dépensées",
userNotUsedAddress: "Vous n'avez pas utilisé cette adresse",
userUsedAddress: "Vous avez utilisé cette adresse",
viewKey: "Clé de visibilité",
viewOnlyMode: "Mode d'affichage seulement. Veuillez charger le portefeuille complet pour envoyer des pièces."
},
titles: {
addressBook: "Carnet d'adresses",
addressDetails: "Détails de l'adresse",
advanced: {
checkTransaction: "CHECK TRANSACTION",
prove: "PROVE"
},
changePassword: "Changer de mot de passe",
configure: "Configurer",
currentlyStakedNodes: "Currently staked nodes",
privateKeys: "Clés privées",
rescanWallet: "Analysez de nouveau le portefeuille",
serviceNode: {
registration: "ENREGISTREMENT",
staking: "STAKING"
},
settings: {
title: "Réglages",
tabs: {
general: "Général",
language: "Langue",
peers: "Pairs"
}
},
transactionDetails: "Détails de la transaction",
transactions: "Transactions",
wallet: {
createNew: "Créer un nouveau portefeuille",
createdOrRestored: "Créer ou restaurer un portefeuille",
importFromFile: "Importer un portefeuille à partir d'un fichier",
importFromLegacyGUI: "Importer un portefeuille à partir d'un héritage GUI",
importFromOldGUI: "Importer un portefeuille à partir d'un ancien GUI",
restoreFromSeed: "Restaurer un portefeuille à partir de mots clés",
restoreViewOnly: "Restaurer un portefeuille en mode d'affichage seulement"
},
welcome: "Bienvenue",
yourWallets: "Vos portefeuilles"
}
};

View File

@ -1,18 +1,18 @@
import enUS from "./en-us"
import enUS from "./en-us";
// ADD LANGUAGES HERE
const languages = [
{ name: "English", code: "en-us", flag: "gb" },
{ name: "Russian", code: "ru", flag: "ru" },
{ name: "German", code: "de", flag: "de" },
{ name: "French", code: "fr", flag: "fr" },
{ name: "Spanish", code: "es", flag: "es" },
{ name: "Portuguese", code: "pt-br", flag: "pt" }
]
{ name: "English", code: "en-us", flag: "gb" },
{ name: "Russian", code: "ru", flag: "ru" },
{ name: "German", code: "de", flag: "de" },
{ name: "French", code: "fr", flag: "fr" },
{ name: "Spanish", code: "es", flag: "es" },
{ name: "Portuguese", code: "pt-br", flag: "pt" }
];
export { languages }
export { languages };
// DO NOT MODIFY THIS EXPORT, LANGUAGE FILES CAN BE DYNAMICALLY LOADED
export default {
"en-us": enUS
}
"en-us": enUS
};

View File

@ -1,490 +1,501 @@
export default {
buttons: {
// All button text is uppercased in the gui
advanced: "ADVANCED",
all: "TUDO",
back: "ATRÁS",
browse: "PESQUISAR",
cancel: "CANCELAR",
change: "MUDAR",
check: "CHECK",
clear: "CLEAR",
close: "FECHAR",
contacts: "CONTACTOS",
copyAddress: "COPIAR ENDEREÇO",
copySignature: "COPY SIGNATURE",
createWallet: "CRIAR CARTEIRA",
delete: "APAGAR",
edit: "EDITAR",
export: "EXPORTAR",
generate: "GENERATE",
import: "IMPORTAR",
importWallet: "IMPORTAR CARTEIRA | IMPORTAR CARTEIRAS",
next: "PRÓXIMO",
openWallet: "ABRIR CARTEIRA",
receive: "RECEBER",
registerServiceNode: "REGISTAR NÓDULO DE SERVIÇO",
rescan: "REEXAMINAR",
restoreWallet: "RESTAURAR CARTEIRA",
save: "SALVAR",
saveTxNotes: "SALVAR NOTAS DE TX",
selectLocation: "SELECIONAR LOCAL",
selectWalletFile: "SELECIONAR FICHEIRO DA CARTEIRA",
send: "ENVIAR",
sendCoins: "ENVIAR MOEDAS",
serviceNode: "NÓDULO DE SERVIÇO",
settings: "CONFIGURAÇÕES",
showQRCode: "MOSTRAR CÓDIGO QR",
showTxDetails: "MOSTRAR DETALHES DA TX",
stake: "PARTICIPAÇÃO",
sweepAll: "VARRER TUDO",
unlock: "UNLOCK",
viewOnExplorer: "VISUALIZAR NO EXPLORADOR"
},
dialog: {
// Generic buttons
buttons: {
// All button text is uppercased in the gui
advanced: "ADVANCED",
all: "TUDO",
back: "ATRÁS",
browse: "PESQUISAR",
cancel: "CANCELAR",
change: "MUDAR",
check: "CHECK",
clear: "CLEAR",
close: "FECHAR",
contacts: "CONTACTOS",
copyAddress: "COPIAR ENDEREÇO",
copySignature: "COPY SIGNATURE",
createWallet: "CRIAR CARTEIRA",
delete: "APAGAR",
edit: "EDITAR",
export: "EXPORTAR",
generate: "GENERATE",
import: "IMPORTAR",
importWallet: "IMPORTAR CARTEIRA | IMPORTAR CARTEIRAS",
next: "PRÓXIMO",
openWallet: "ABRIR CARTEIRA",
receive: "RECEBER",
registerServiceNode: "REGISTAR NÓDULO DE SERVIÇO",
rescan: "REEXAMINAR",
restoreWallet: "RESTAURAR CARTEIRA",
save: "SALVAR",
saveTxNotes: "SALVAR NOTAS DE TX",
selectLocation: "SELECIONAR LOCAL",
selectWalletFile: "SELECIONAR FICHEIRO DA CARTEIRA",
send: "ENVIAR",
sendCoins: "ENVIAR MOEDAS",
serviceNode: "NÓDULO DE SERVIÇO",
settings: "CONFIGURAÇÕES",
showQRCode: "MOSTRAR CÓDIGO QR",
showTxDetails: "MOSTRAR DETALHES DA TX",
stake: "PARTICIPAÇÃO",
sweepAll: "VARRER TUDO",
unlock: "UNLOCK",
viewOnExplorer: "VISUALIZAR NO EXPLORADOR"
ok: "OK",
cancel: "CANCELAR",
open: "ABRIR"
},
dialog: {
// Generic buttons
buttons: {
ok: "OK",
cancel: "CANCELAR",
open: "ABRIR"
},
// Dialogs
banPeer: {
title: "Banir pares",
peerDetailsTitle: "Detalhes dos pares",
message: "Introduzir duração para banir pares, em segundos.\nDefault 3600 = 1 hora.",
ok: "Banir par"
},
copyAddress: {
title: "Copiar endereço",
message: "Existe um id de pagamento associado a este endereço.\nTenha a certeza de copiar este id de pagamanto separadamente."
},
copyPrivateKeys: {
// Copy {seedWords/viewKey/spendKey}
title: "Copiar {type}",
message: "Tenha cuidado a quem envia as suas chaves privadas, pois elas controlam os seus fundos.",
seedWords: "Palavras Semente",
viewKey: "Chave de Visualização",
spendKey: "Chave de Gasto"
},
deleteWallet: {
title: "Apagar carteira",
message: "Tem a certeza absoluta que pretende apagar a sua carteira?\nTenha a certeza que tem um backup da suas chaves privadas.\nESTE PROCESSO NÃO É REVERSÍVEL!",
ok: "APAGAR"
},
exit: {
title: "Sair",
message: "Tem a certeza que deseja sair?",
ok: "SAIR"
},
keyImages: {
title: "{type} imagens chave",
message: "Quer {type} imagens chave?",
export: "Exportar",
import: "Importar"
},
noPassword: {
title: "Nenhum palavra-passe definida",
message: "Tem a certeza que deseja criar uma carteira sem palavra-passe?",
ok: "SIM"
},
password: {
title: "Palavra-passe",
message: "Introduza a sua palavra-passe para continuar"
},
registerServiceNode: {
title: "Registar um nódulo de serviço",
message: "Pretende registar um nódulo de serviço?",
ok: "REGISTAR"
},
rescan: {
title: "Reexaminar carteira",
message: "Aviso: Algumas informações acerca de transações passadas serão perdidas,\ntal como o endereço dos recipientes que serão perdidos.",
ok: "REEXAMINAR"
},
restart: {
title: "Reiniciar",
message: "Alterações requerem um reiniciar. Quer reiniciar agora?",
ok: "Reiniciar"
},
showPrivateKeys: {
title: "Mostrar chaves privadas",
message: "Deseja ver as suas chaves privadas?",
ok: "MOSTRAR"
},
stake: {
title: "Participação num nódulo de serviço",
message: "Deseja participar num nódulo de serviço?",
ok: "Participação"
},
sweepAll: {
title: "Varrer Tudo",
message: "Quer realmente varrer tudo?",
ok: "VARRER TUDO"
},
sweepAllWarning: {
title: "Sweep all warning",
message: "You are about to combine all of your unspent funds by sending a transaction to yourself, your wallet may show a balance of 0 temporarily, after 10 blocks your funds will unlock and you may stake normally.",
ok: "CONTINUE"
},
switchWallet: {
title: "Mudar de carteira",
closeMessage: "Tem a certeza que deseja fechar a carteira actual?",
restartMessage: "A carteira RPC está correntemente em sincronização. \nISe deseja mudar de carteira terá de reiniciar a aplicação. \nPerderá o progresso da sincronização e terá de reexaminar o blockchain."
},
transactionDetails: {
title: "Detalhes das transações",
ok: "FECHAR"
},
transfer: {
title: "Transferir",
message: "Deseja enviar a transação?",
ok: "ENVIAR"
},
unlockConfirm: {
title: "Confirmar desbloqueamento",
ok: "DESBLOQUEAR"
},
unlockServiceNode: {
title: "Desbloquear nódulo de serviço",
confirmTitle: "Confirmar desbloqueio",
message: "Deseja desbloquear o nódulo de serviço?",
ok: "DESBLOQUEAR"
},
unlockServiceNodeWarning: {
title: "Unlock service node warning",
message: "Unlocking a partial stake in a node will also unstake for any other participants, if staking in a shared node its best to let the operator and other participants know you are unstaking.",
ok: "CONTINUE"
}
// Dialogs
banPeer: {
title: "Banir pares",
peerDetailsTitle: "Detalhes dos pares",
message: "Introduzir duração para banir pares, em segundos.\nDefault 3600 = 1 hora.",
ok: "Banir par"
},
fieldLabels: {
// Field labels are also all uppercased
address: "ENDEREÇO",
amount: "QUANTIDADE",
confirmPassword: "CONFIRMAR PALAVRA-PASSE",
daemonLogLevel: "NÍVEL DE LOG PARA O SERVIÇO",
daemonP2pPort: "PORTA P2P DO SERVIÇO",
daemonZMQPort: "PORTA ZMQ DO SERVIÇO",
dataStoragePath: "DIRECTÓRIO DE ARMAZENAMENTO DOS DADOS",
filter: "FILTER",
filterTransactionType: "FILTRAR POR TIPO DE TRANSAÇÃO",
internalWalletPort: "PORTA INTERNA DA CARTEIRA",
keyImages: {
exportDirectory: "DIRETORIA DE EXPORTAÇÃO DAS CHAVES DE IMAGEM",
importFile: "FICHEIRO DE IMPORTAÇÃO DAS CHAVES DE IMAGEM"
},
limitDownloadRate: "LIMITAR VELOCIDADE DE DOWNLOAD",
limitUploadRate: "LIMITE DA VELOCIDADE DE UPLOAD",
localDaemonIP: "IP LOCAL DO SERVIÇO",
localDaemonPort: "PORTA LOCAL DO SERVIÇO",
maxIncomingPeers: "NÚM. MAX DE PARES DE ENTRADA",
maxOutgoingPeers: "NUM. MAX DE PARES DE SAÍDA",
message: "MESSAGE",
mnemonicSeed: "SEMENTE MNEMÓNICA",
name: "NOME",
newWalletName: "NOME DA NOVA CARTEIRA",
notes: "NOTAS",
optional: "OPCIONAL",
password: "PALAVRA-PASSE",
paymentId: "ID DE PAGAMENTO",
priority: "PRIORIDADE",
remoteNodeHost: "NÓDULO HÓSPEDE REMOTO",
remoteNodePort: "PORTA REMOTA DO NÓDULO",
restoreFromBlockHeight: "RESTAURE DESDE ALTURA DO BLOCO",
restoreFromDate: "RESTAURAR DESDE DATA",
seedLanguage: "LINGUA DA SEMENTE",
serviceNodeCommand: "COMANDO DO NÓDULO DE SERVIÇO",
serviceNodeKey: "CHAVE DO NÓDULO DE SERVIÇO",
signature: "SIGNATURE",
transactionId: "TRANSACTION ID",
walletFile: "FICHEIRO DA CARTEIRA",
walletLogLevel: "NIVEL DE LOG DA CARTEIRA",
walletName: "NOME DA CARTEIRA",
walletRPCPort: "PORTA RPC DA CARTEIRA",
walletStoragePath: "DIRECTORIA DE SALVAMENTO DA CARTEIRA",
// These are specific labels which do not get uppercased
confirmNewPassword: "Confirmar Nova Palavra-Passe",
newPassword: "Nova Palavra-Passe",
oldPassword: "Antiga Palavra-Passe",
rescanFullBlockchain: "Reexaminar o blockchain completo",
rescanSpentOutputs: "Reexaminar saídas gastas",
transactionNotes: "Notas de Transações",
chooseNetwork: "Seleccione uma Rede",
network: "Rede"
copyAddress: {
title: "Copiar endereço",
message:
"Existe um id de pagamento associado a este endereço.\nTenha a certeza de copiar este id de pagamanto separadamente."
},
footer: {
ready: "Pronto",
scanning: "Examinando",
status: "Estado",
syncing: "SINCRONIZANDO",
remote: "Remoto",
wallet: "Carteira"
copyPrivateKeys: {
// Copy {seedWords/viewKey/spendKey}
title: "Copiar {type}",
message: "Tenha cuidado a quem envia as suas chaves privadas, pois elas controlam os seus fundos.",
seedWords: "Palavras Semente",
viewKey: "Chave de Visualização",
spendKey: "Chave de Gasto"
},
menuItems: {
about: "Sobre",
changePassword: "Alterar Palavra-Passe",
copyAddress: "Copiar Endereço",
copyQR: "Copiar código QR",
copySeedWords: "Copiar palavras semente",
copySpendKey: "Copiar chave de gasto",
copyServiceNodeKey: "Copy service node key",
copyTransactionId: "Copira ID da transação",
copyViewKey: "Copiar chave de visualização",
createNewWallet: "Criar nova carteira",
deleteWallet: "Apagar carteira",
exit: "Saír da Carteira GUI Loki",
importOldGUIWallet: "Importar carteiras da antiga carteira GUI",
manageKeyImages: "Administrar Imagens de Chave",
openWallet: "Abrir carteira",
rescanWallet: "Reexaminar Carteira",
restoreWalletFile: "Restaurar carteira a partir de ficheiro",
restoreWalletSeed: "Restaurar carteira a partir da semente",
saveQR: "Salvar código QR para um ficheiro",
sendToThisAddress: "Enviar para este endereço",
settings: "Configurações",
showDetails: "Mostrar detalhes",
showPrivateKeys: "Mostrar Chaves Privadas",
showQRCode: "Mostrar Código QR",
switchWallet: "Mudar de Carteira",
viewOnExplorer: "Visualizar no explorador"
deleteWallet: {
title: "Apagar carteira",
message:
"Tem a certeza absoluta que pretende apagar a sua carteira?\nTenha a certeza que tem um backup da suas chaves privadas.\nESTE PROCESSO NÃO É REVERSÍVEL!",
ok: "APAGAR"
},
notification: {
positive: {
addressCopied: "Copiar endereço para o clipboard",
bannedPeer: "{host} banido até {time}",
copied: "{item} copiado para o clipboard",
itemSaved: "{item} salvado em {filename}",
keyImages: {
exported: "Imagens de Chave exportadas para {filename}",
imported: "Imagens de Chave importadas"
},
passwordUpdated: "Palavra-Passe actualizada",
qrCopied: "Código QR ccopiado para o clipboard",
registerServiceNodeSuccess: "Nódulo de serviço registado com sucesso",
sendSuccess: "Transação enviada com sucesso.",
signatureCopied: "Signature copied to clipboard",
stakeSuccess: "Participação com sucesso",
transactionNotesSaved: "A nota de transação foi salva"
},
errors: {
banningPeer: "Erro ao banir par",
cannotAccessRemoteNode: "Não foi possível aceder ao nódulo remoto, por favor experimente outro nódulo remoto",
changingPassword: "Erro ao alterar palabra-passe",
copyWalletFail: "Cópia da carteira falhou",
copyingPrivateKeys: "Erro ao copiar as chaves privadas",
dataPathNotFound: "Directoria de armazenamento de dados não encontrada",
differentNetType: "Nódulo remoto está usando um tipo de rede diferente",
enterSeedWords: "Introduzir palavras semente",
enterTransactionId: "Enter transaction ID",
enterTransactionProof: "Enter transaction proof",
enterWalletName: "Introduza o nome da carteira",
errorSavingItem: "Erro salvando {item}",
failedServiceNodeUnlock: "Falhou o desbloqueamento do nódulo de serviço",
failedToSetLanguage: "Falhou mudança de língua: {lang}",
failedWalletImport: "Falhou importação da carteira",
failedWalletOpen: "Falhou abertura da carteira. Por favor tente de novo.",
internalError: "Erro interno",
invalidAddress: "Endereço não válido",
invalidAmount: "Quantidade não válida",
invalidOldPassword: "Palavra-passe antiga inválida",
invalidPassword: "Palavra-passe inválida",
invalidPaymentId: "ID de pagamento inválido",
invalidPrivateViewKey: "Chave de visualização privada inválida",
invalidPublicAddress: "Endereço público inválido",
invalidRestoreDate: "Data de restauro inválido",
invalidRestoreHeight: "Altura de restauro inválido",
invalidSeedLength: "Comprimento da palavra semente inválido",
invalidServiceNodeCommand: "Por favor introduza o comando de registro do nódulo de serviço",
invalidServiceNodeKey: "Chave do nódulo de serviço inválido",
invalidWalletPath: "Caminho da carteira inválido",
keyImages: {
exporting: "Erro ao exportar as chaves de imagem",
reading: "Erro ao ler as chaves de imagem",
importing: "Erro ao importar as chaves de imagem"
},
negativeAmount: "Quantidade não pode ser negativa",
newPasswordNoMatch: "Nova palavra-passe não coincide",
newPasswordSame: "Nova palavra-passe tem de ser diferente",
notEnoughBalance: "Não existe saldo desbloqueado suficiente",
passwordNoMatch: "Palavra-passe não coincidem",
remoteCannotBeReached: "Não é possivel contactar o nódulo Remoto",
selectWalletFile: "Seleccione um ficheiro de carteira",
unknownError: "Ocorreu um error desconhecido",
walletAlreadyExists: "Carteira com esse nome já existe",
walletPathNotFound: "Caminho de armazenamento dos dados da carteira não encontrado",
zeroAmount: "Quantidade tem de ser superior a zero"
},
warnings: {
noKeyImageExport: "Nenhuma chave de imagem encontrada para exportar",
usingLocalNode: "Não foi possível aceder ao nódulo remoto, mudando para nódulo local apenas",
usingRemoteNode: "lokid não encontrado, utilizando nódulo remoto"
}
exit: {
title: "Sair",
message: "Tem a certeza que deseja sair?",
ok: "SAIR"
},
placeholders: {
additionalNotes: "Notas adicionais",
addressBookName: "Nome que pertence a este endereço",
filterTx: "Enter an ID, name, address or amount",
hexCharacters: "{count} caracteres hexadecimais",
mnemonicSeed: "25 (or 24) palavras semente mnemónicas",
pasteTransactionId: "Paste transaction ID",
pasteTransactionProof: "Paste transaction proof",
proveOptionalMessage: "Optional message against which the signature is signed",
recipientWalletAddress: "Recipient's wallet address",
selectAFile: "Por favor selecione um ficheiro",
transactionNotes: "Notas adicionais para anexar à transação",
walletName: "Um nome para a sua carteira",
walletPassword: "Uma palavra-passe opcional para a carteira"
keyImages: {
title: "{type} imagens chave",
message: "Quer {type} imagens chave?",
export: "Exportar",
import: "Importar"
},
strings: {
addAddressBookEntry: "Adicionar registo ao livro de endereços",
addressBookDetails: "Detalhes do livro de endereço",
addressBookIsEmpty: "Livro de endereço vazio",
addresses: {
myPrimaryAddress: "Meu endereço primario",
myUnusedAddresses: "Meus endereços não utilizados",
myUsedAddresses: "Meus endereços utilizados",
primaryAddress: "Endereço primario",
subAddress: "Sub-endereço",
subAddressIndex: "Índice {index}"
},
advancedOptions: "Opções Avançadas",
bannedPeers: {
title: "Pares banidos (banimento serão anulados ao reiniciar a carteira)",
bannedUntil: "Banido até {time}"
},
blockHeight: "Altura",
checkTransaction: {
description: "Verify that funds were paid to an address by supplying the transaction ID, the recipient address, the message used for signing and the signature.\nFor a 'Spend Proof' you dont need to provide the recipient address.",
infoTitles: {
confirmations: "Confirmations",
inPool: "In pool",
validTransaction: "Valid transaction",
received: "Received amount"
},
validTransaction: {
no: "NO",
yes: "YES"
}
},
closing: "Fechando",
connectingToBackend: "Conectado ao backend",
contribution: "Contribution",
daemon: {
local: {
title: "Serviço Local Apenas",
description: "Segurança total, a carteira irá baixar o blockchain completo. Não será capaz de transaccionar até a sincronização completar."
},
localRemote: {
title: "Serviço Local + Remoto",
description: "Comece rápidamente com esta opção padrão. A carteira irá baixar o blockchain completo, mas usar um nódulo remoto enquanto sincroniza."
},
remote: {
title: "Serviço Remoto Apenas",
description: "Menos segurança, a carteira irá conectar a um nódulo remoto para todas as transações."
}
},
destinationUnknown: "Destino Desconhecido",
editAddressBookEntry: "Editar registo do livro de endereços",
loadingSettings: "Carregando configurações",
lokiBalance: "Saldo",
lokiUnlockedBalance: "Saldo desbloqueado",
lokiUnlockedShort: "Desbloqueado",
noTransactionsFound: "Nenhuma transação encontrada",
notes: "Notas",
numberOfUnspentOutputs: "Número de outputs não gastos",
paymentID: "ID de Pagamento",
peerList: "Lista de pares",
priorityOptions: {
automatic: "Automatico",
slow: "Lento",
normal: "Normal",
fast: "Rápido",
fastest: "O Mais Rápido"
},
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: "Lendo lista da carteira",
recentIncomingTransactionsToAddress: "Transações de entrada neste endereço",
recentTransactionsWithAddress: "Transações recentes neste endereço",
rescanModalDescription: "Seleccione examinação completa ou examinação de outputs gastos apenas.",
saveSeedWarning: "Por favor copie e salve estes num lugar seguro!",
saveToAddressBook: "Salvar para o livro de endereço",
seedWords: "Palavras semente",
selectLanguage: "Selecione língua",
serviceNodeRegistrationDescription: "Introduza o {registerCommand} commando produzido pelo serviço que está se registando para se tornar un Nódulo de Serviço utilizando o commando \"{prepareCommand}\" ",
spendKey: "Chave de gasto",
startingDaemon: "Começando serviço",
startingWallet: "Começando carteira",
switchToDateSelect: "Mudar para data de seleção",
switchToHeightSelect: "Mudar para altura de seleção",
transactionID: "ID da Transação",
transactionConfirmed: "confirmado",
transactions: {
amount: "Quantidade",
description: "{type} transação",
fee: "Taxa",
paidBySender: "pago por remetente",
received: "Recebido",
sent: "Enviado",
sentTo: "{type} transacão enviada a",
timestamp: "Data e hora",
types: {
all: "Todos",
incoming: "Entradas",
outgoing: "Saídas",
pending: "Pendentes",
pendingIncoming: "Entradas pendentes",
pendingOutgoing: "Saídas pendentes",
miner: "Mineiro",
serviceNode: "Nódulo de Serviço",
governance: "Governança",
stake: "Participação",
failed: "Falhou"
}
},
unspentOutputs: "Outputs não-gastos",
userNotUsedAddress: "Não utilizou este endereço",
userUsedAddress: "Já utilizou este endereço",
viewKey: "Chave de visualização",
viewOnlyMode: "Modo de visualização apenas. Por favor carregue a carteira completa para poder enviar moedas."
noPassword: {
title: "Nenhum palavra-passe definida",
message: "Tem a certeza que deseja criar uma carteira sem palavra-passe?",
ok: "SIM"
},
titles: {
addressBook: "Livro de endereços",
addressDetails: "Detalhes do endereço",
advanced: {
checkTransaction: "CHECK TRANSACTION",
prove: "PROVE"
},
changePassword: "Mudar palavra-passe",
configure: "Configure",
currentlyStakedNodes: "Currently staked nodes",
privateKeys: "Chaves privadas",
rescanWallet: "Reexaminar carteira",
serviceNode: {
registration: "REGISTO",
staking: "PARTICIPAÇÃO"
},
settings: {
title: "Configurações",
tabs: {
general: "Geral",
language: "Língua",
peers: "Pares"
}
},
transactionDetails: "Detalhes das transações",
transactions: "Transações",
wallet: {
createNew: "Criar nova carteira",
createdOrRestored: "Carteira criada/restaurada",
importFromFile: "Importar carteira a partir de ficheiro",
importFromLegacyGUI: "Importar carteira a partir da antiga carteira GUI (legado)",
importFromOldGUI: "Importar carteira a partir da antiga carteira GUI",
restoreFromSeed: "Restaurar carteira a partir de semente",
restoreViewOnly: "Restaurar carteira visualização-apenas"
},
welcome: "Bem-vindo",
yourWallets: "Suas carteiras"
password: {
title: "Palavra-passe",
message: "Introduza a sua palavra-passe para continuar"
},
registerServiceNode: {
title: "Registar um nódulo de serviço",
message: "Pretende registar um nódulo de serviço?",
ok: "REGISTAR"
},
rescan: {
title: "Reexaminar carteira",
message:
"Aviso: Algumas informações acerca de transações passadas serão perdidas,\ntal como o endereço dos recipientes que serão perdidos.",
ok: "REEXAMINAR"
},
restart: {
title: "Reiniciar",
message: "Alterações requerem um reiniciar. Quer reiniciar agora?",
ok: "Reiniciar"
},
showPrivateKeys: {
title: "Mostrar chaves privadas",
message: "Deseja ver as suas chaves privadas?",
ok: "MOSTRAR"
},
stake: {
title: "Participação num nódulo de serviço",
message: "Deseja participar num nódulo de serviço?",
ok: "Participação"
},
sweepAll: {
title: "Varrer Tudo",
message: "Quer realmente varrer tudo?",
ok: "VARRER TUDO"
},
sweepAllWarning: {
title: "Sweep all warning",
message:
"You are about to combine all of your unspent funds by sending a transaction to yourself, your wallet may show a balance of 0 temporarily, after 10 blocks your funds will unlock and you may stake normally.",
ok: "CONTINUE"
},
switchWallet: {
title: "Mudar de carteira",
closeMessage: "Tem a certeza que deseja fechar a carteira actual?",
restartMessage:
"A carteira RPC está correntemente em sincronização. \nISe deseja mudar de carteira terá de reiniciar a aplicação. \nPerderá o progresso da sincronização e terá de reexaminar o blockchain."
},
transactionDetails: {
title: "Detalhes das transações",
ok: "FECHAR"
},
transfer: {
title: "Transferir",
message: "Deseja enviar a transação?",
ok: "ENVIAR"
},
unlockConfirm: {
title: "Confirmar desbloqueamento",
ok: "DESBLOQUEAR"
},
unlockServiceNode: {
title: "Desbloquear nódulo de serviço",
confirmTitle: "Confirmar desbloqueio",
message: "Deseja desbloquear o nódulo de serviço?",
ok: "DESBLOQUEAR"
},
unlockServiceNodeWarning: {
title: "Unlock service node warning",
message:
"Unlocking a partial stake in a node will also unstake for any other participants, if staking in a shared node its best to let the operator and other participants know you are unstaking.",
ok: "CONTINUE"
}
}
},
fieldLabels: {
// Field labels are also all uppercased
address: "ENDEREÇO",
amount: "QUANTIDADE",
confirmPassword: "CONFIRMAR PALAVRA-PASSE",
daemonLogLevel: "NÍVEL DE LOG PARA O SERVIÇO",
daemonP2pPort: "PORTA P2P DO SERVIÇO",
daemonZMQPort: "PORTA ZMQ DO SERVIÇO",
dataStoragePath: "DIRECTÓRIO DE ARMAZENAMENTO DOS DADOS",
filter: "FILTER",
filterTransactionType: "FILTRAR POR TIPO DE TRANSAÇÃO",
internalWalletPort: "PORTA INTERNA DA CARTEIRA",
keyImages: {
exportDirectory: "DIRETORIA DE EXPORTAÇÃO DAS CHAVES DE IMAGEM",
importFile: "FICHEIRO DE IMPORTAÇÃO DAS CHAVES DE IMAGEM"
},
limitDownloadRate: "LIMITAR VELOCIDADE DE DOWNLOAD",
limitUploadRate: "LIMITE DA VELOCIDADE DE UPLOAD",
localDaemonIP: "IP LOCAL DO SERVIÇO",
localDaemonPort: "PORTA LOCAL DO SERVIÇO",
maxIncomingPeers: "NÚM. MAX DE PARES DE ENTRADA",
maxOutgoingPeers: "NUM. MAX DE PARES DE SAÍDA",
message: "MESSAGE",
mnemonicSeed: "SEMENTE MNEMÓNICA",
name: "NOME",
newWalletName: "NOME DA NOVA CARTEIRA",
notes: "NOTAS",
optional: "OPCIONAL",
password: "PALAVRA-PASSE",
paymentId: "ID DE PAGAMENTO",
priority: "PRIORIDADE",
remoteNodeHost: "NÓDULO HÓSPEDE REMOTO",
remoteNodePort: "PORTA REMOTA DO NÓDULO",
restoreFromBlockHeight: "RESTAURE DESDE ALTURA DO BLOCO",
restoreFromDate: "RESTAURAR DESDE DATA",
seedLanguage: "LINGUA DA SEMENTE",
serviceNodeCommand: "COMANDO DO NÓDULO DE SERVIÇO",
serviceNodeKey: "CHAVE DO NÓDULO DE SERVIÇO",
signature: "SIGNATURE",
transactionId: "TRANSACTION ID",
walletFile: "FICHEIRO DA CARTEIRA",
walletLogLevel: "NIVEL DE LOG DA CARTEIRA",
walletName: "NOME DA CARTEIRA",
walletRPCPort: "PORTA RPC DA CARTEIRA",
walletStoragePath: "DIRECTORIA DE SALVAMENTO DA CARTEIRA",
// These are specific labels which do not get uppercased
confirmNewPassword: "Confirmar Nova Palavra-Passe",
newPassword: "Nova Palavra-Passe",
oldPassword: "Antiga Palavra-Passe",
rescanFullBlockchain: "Reexaminar o blockchain completo",
rescanSpentOutputs: "Reexaminar saídas gastas",
transactionNotes: "Notas de Transações",
chooseNetwork: "Seleccione uma Rede",
network: "Rede"
},
footer: {
ready: "Pronto",
scanning: "Examinando",
status: "Estado",
syncing: "SINCRONIZANDO",
remote: "Remoto",
wallet: "Carteira"
},
menuItems: {
about: "Sobre",
changePassword: "Alterar Palavra-Passe",
copyAddress: "Copiar Endereço",
copyQR: "Copiar código QR",
copySeedWords: "Copiar palavras semente",
copySpendKey: "Copiar chave de gasto",
copyServiceNodeKey: "Copy service node key",
copyTransactionId: "Copira ID da transação",
copyViewKey: "Copiar chave de visualização",
createNewWallet: "Criar nova carteira",
deleteWallet: "Apagar carteira",
exit: "Saír da Carteira GUI Loki",
importOldGUIWallet: "Importar carteiras da antiga carteira GUI",
manageKeyImages: "Administrar Imagens de Chave",
openWallet: "Abrir carteira",
rescanWallet: "Reexaminar Carteira",
restoreWalletFile: "Restaurar carteira a partir de ficheiro",
restoreWalletSeed: "Restaurar carteira a partir da semente",
saveQR: "Salvar código QR para um ficheiro",
sendToThisAddress: "Enviar para este endereço",
settings: "Configurações",
showDetails: "Mostrar detalhes",
showPrivateKeys: "Mostrar Chaves Privadas",
showQRCode: "Mostrar Código QR",
switchWallet: "Mudar de Carteira",
viewOnExplorer: "Visualizar no explorador"
},
notification: {
positive: {
addressCopied: "Copiar endereço para o clipboard",
bannedPeer: "{host} banido até {time}",
copied: "{item} copiado para o clipboard",
itemSaved: "{item} salvado em {filename}",
keyImages: {
exported: "Imagens de Chave exportadas para {filename}",
imported: "Imagens de Chave importadas"
},
passwordUpdated: "Palavra-Passe actualizada",
qrCopied: "Código QR ccopiado para o clipboard",
registerServiceNodeSuccess: "Nódulo de serviço registado com sucesso",
sendSuccess: "Transação enviada com sucesso.",
signatureCopied: "Signature copied to clipboard",
stakeSuccess: "Participação com sucesso",
transactionNotesSaved: "A nota de transação foi salva"
},
errors: {
banningPeer: "Erro ao banir par",
cannotAccessRemoteNode: "Não foi possível aceder ao nódulo remoto, por favor experimente outro nódulo remoto",
changingPassword: "Erro ao alterar palabra-passe",
copyWalletFail: "Cópia da carteira falhou",
copyingPrivateKeys: "Erro ao copiar as chaves privadas",
dataPathNotFound: "Directoria de armazenamento de dados não encontrada",
differentNetType: "Nódulo remoto está usando um tipo de rede diferente",
enterSeedWords: "Introduzir palavras semente",
enterTransactionId: "Enter transaction ID",
enterTransactionProof: "Enter transaction proof",
enterWalletName: "Introduza o nome da carteira",
errorSavingItem: "Erro salvando {item}",
failedServiceNodeUnlock: "Falhou o desbloqueamento do nódulo de serviço",
failedToSetLanguage: "Falhou mudança de língua: {lang}",
failedWalletImport: "Falhou importação da carteira",
failedWalletOpen: "Falhou abertura da carteira. Por favor tente de novo.",
internalError: "Erro interno",
invalidAddress: "Endereço não válido",
invalidAmount: "Quantidade não válida",
invalidOldPassword: "Palavra-passe antiga inválida",
invalidPassword: "Palavra-passe inválida",
invalidPaymentId: "ID de pagamento inválido",
invalidPrivateViewKey: "Chave de visualização privada inválida",
invalidPublicAddress: "Endereço público inválido",
invalidRestoreDate: "Data de restauro inválido",
invalidRestoreHeight: "Altura de restauro inválido",
invalidSeedLength: "Comprimento da palavra semente inválido",
invalidServiceNodeCommand: "Por favor introduza o comando de registro do nódulo de serviço",
invalidServiceNodeKey: "Chave do nódulo de serviço inválido",
invalidWalletPath: "Caminho da carteira inválido",
keyImages: {
exporting: "Erro ao exportar as chaves de imagem",
reading: "Erro ao ler as chaves de imagem",
importing: "Erro ao importar as chaves de imagem"
},
negativeAmount: "Quantidade não pode ser negativa",
newPasswordNoMatch: "Nova palavra-passe não coincide",
newPasswordSame: "Nova palavra-passe tem de ser diferente",
notEnoughBalance: "Não existe saldo desbloqueado suficiente",
passwordNoMatch: "Palavra-passe não coincidem",
remoteCannotBeReached: "Não é possivel contactar o nódulo Remoto",
selectWalletFile: "Seleccione um ficheiro de carteira",
unknownError: "Ocorreu um error desconhecido",
walletAlreadyExists: "Carteira com esse nome já existe",
walletPathNotFound: "Caminho de armazenamento dos dados da carteira não encontrado",
zeroAmount: "Quantidade tem de ser superior a zero"
},
warnings: {
noKeyImageExport: "Nenhuma chave de imagem encontrada para exportar",
usingLocalNode: "Não foi possível aceder ao nódulo remoto, mudando para nódulo local apenas",
usingRemoteNode: "lokid não encontrado, utilizando nódulo remoto"
}
},
placeholders: {
additionalNotes: "Notas adicionais",
addressBookName: "Nome que pertence a este endereço",
filterTx: "Enter an ID, name, address or amount",
hexCharacters: "{count} caracteres hexadecimais",
mnemonicSeed: "25 (or 24) palavras semente mnemónicas",
pasteTransactionId: "Paste transaction ID",
pasteTransactionProof: "Paste transaction proof",
proveOptionalMessage: "Optional message against which the signature is signed",
recipientWalletAddress: "Recipient's wallet address",
selectAFile: "Por favor selecione um ficheiro",
transactionNotes: "Notas adicionais para anexar à transação",
walletName: "Um nome para a sua carteira",
walletPassword: "Uma palavra-passe opcional para a carteira"
},
strings: {
addAddressBookEntry: "Adicionar registo ao livro de endereços",
addressBookDetails: "Detalhes do livro de endereço",
addressBookIsEmpty: "Livro de endereço vazio",
addresses: {
myPrimaryAddress: "Meu endereço primario",
myUnusedAddresses: "Meus endereços não utilizados",
myUsedAddresses: "Meus endereços utilizados",
primaryAddress: "Endereço primario",
subAddress: "Sub-endereço",
subAddressIndex: "Índice {index}"
},
advancedOptions: "Opções Avançadas",
bannedPeers: {
title: "Pares banidos (banimento serão anulados ao reiniciar a carteira)",
bannedUntil: "Banido até {time}"
},
blockHeight: "Altura",
checkTransaction: {
description:
"Verify that funds were paid to an address by supplying the transaction ID, the recipient address, the message used for signing and the signature.\nFor a 'Spend Proof' you dont need to provide the recipient address.",
infoTitles: {
confirmations: "Confirmations",
inPool: "In pool",
validTransaction: "Valid transaction",
received: "Received amount"
},
validTransaction: {
no: "NO",
yes: "YES"
}
},
closing: "Fechando",
connectingToBackend: "Conectado ao backend",
contribution: "Contribution",
daemon: {
local: {
title: "Serviço Local Apenas",
description:
"Segurança total, a carteira irá baixar o blockchain completo. Não será capaz de transaccionar até a sincronização completar."
},
localRemote: {
title: "Serviço Local + Remoto",
description:
"Comece rápidamente com esta opção padrão. A carteira irá baixar o blockchain completo, mas usar um nódulo remoto enquanto sincroniza."
},
remote: {
title: "Serviço Remoto Apenas",
description: "Menos segurança, a carteira irá conectar a um nódulo remoto para todas as transações."
}
},
destinationUnknown: "Destino Desconhecido",
editAddressBookEntry: "Editar registo do livro de endereços",
loadingSettings: "Carregando configurações",
lokiBalance: "Saldo",
lokiUnlockedBalance: "Saldo desbloqueado",
lokiUnlockedShort: "Desbloqueado",
noTransactionsFound: "Nenhuma transação encontrada",
notes: "Notas",
numberOfUnspentOutputs: "Número de outputs não gastos",
paymentID: "ID de Pagamento",
peerList: "Lista de pares",
priorityOptions: {
automatic: "Automatico",
slow: "Lento",
normal: "Normal",
fast: "Rápido",
fastest: "O Mais Rápido"
},
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: "Lendo lista da carteira",
recentIncomingTransactionsToAddress: "Transações de entrada neste endereço",
recentTransactionsWithAddress: "Transações recentes neste endereço",
rescanModalDescription: "Seleccione examinação completa ou examinação de outputs gastos apenas.",
saveSeedWarning: "Por favor copie e salve estes num lugar seguro!",
saveToAddressBook: "Salvar para o livro de endereço",
seedWords: "Palavras semente",
selectLanguage: "Selecione língua",
serviceNodeRegistrationDescription:
'Introduza o {registerCommand} commando produzido pelo serviço que está se registando para se tornar un Nódulo de Serviço utilizando o commando "{prepareCommand}" ',
spendKey: "Chave de gasto",
startingDaemon: "Começando serviço",
startingWallet: "Começando carteira",
switchToDateSelect: "Mudar para data de seleção",
switchToHeightSelect: "Mudar para altura de seleção",
transactionID: "ID da Transação",
transactionConfirmed: "confirmado",
transactions: {
amount: "Quantidade",
description: "{type} transação",
fee: "Taxa",
paidBySender: "pago por remetente",
received: "Recebido",
sent: "Enviado",
sentTo: "{type} transacão enviada a",
timestamp: "Data e hora",
types: {
all: "Todos",
incoming: "Entradas",
outgoing: "Saídas",
pending: "Pendentes",
pendingIncoming: "Entradas pendentes",
pendingOutgoing: "Saídas pendentes",
miner: "Mineiro",
serviceNode: "Nódulo de Serviço",
governance: "Governança",
stake: "Participação",
failed: "Falhou"
}
},
unspentOutputs: "Outputs não-gastos",
userNotUsedAddress: "Não utilizou este endereço",
userUsedAddress: "Já utilizou este endereço",
viewKey: "Chave de visualização",
viewOnlyMode: "Modo de visualização apenas. Por favor carregue a carteira completa para poder enviar moedas."
},
titles: {
addressBook: "Livro de endereços",
addressDetails: "Detalhes do endereço",
advanced: {
checkTransaction: "CHECK TRANSACTION",
prove: "PROVE"
},
changePassword: "Mudar palavra-passe",
configure: "Configure",
currentlyStakedNodes: "Currently staked nodes",
privateKeys: "Chaves privadas",
rescanWallet: "Reexaminar carteira",
serviceNode: {
registration: "REGISTO",
staking: "PARTICIPAÇÃO"
},
settings: {
title: "Configurações",
tabs: {
general: "Geral",
language: "Língua",
peers: "Pares"
}
},
transactionDetails: "Detalhes das transações",
transactions: "Transações",
wallet: {
createNew: "Criar nova carteira",
createdOrRestored: "Carteira criada/restaurada",
importFromFile: "Importar carteira a partir de ficheiro",
importFromLegacyGUI: "Importar carteira a partir da antiga carteira GUI (legado)",
importFromOldGUI: "Importar carteira a partir da antiga carteira GUI",
restoreFromSeed: "Restaurar carteira a partir de semente",
restoreViewOnly: "Restaurar carteira visualização-apenas"
},
welcome: "Bem-vindo",
yourWallets: "Suas carteiras"
}
};

View File

@ -1,490 +1,499 @@
export default {
buttons: {
// All button text is uppercased in the gui
advanced: "ADVANCED",
all: "ВСЕ",
back: "НАЗАД",
browse: "ОБЗОР",
cancel: "ОТМЕНА",
change: "ИЗМЕНИТЬ",
check: "CHECK",
clear: "CLEAR",
close: "ЗАКРЫТЬ",
contacts: "КОНТАКТЫ",
copyAddress: "КОПИРОВАТЬ АДРЕС",
copySignature: "COPY SIGNATURE",
createWallet: "СОЗДАТЬ КОШЕЛЕК",
delete: "УДАЛИТЬ",
edit: "РЕДАКТИРОВАТЬ",
export: "ЭКСПОРТИРОВАТЬ",
generate: "GENERATE",
import: "ИМПОРТИРОВАТЬ",
importWallet: "ИМПОРТИРОВАТЬ КОШЕЛЕК | ИМПОРТИРОВАТЬ КОШЕЛЬКИ",
next: "ДАЛЕЕ",
openWallet: "ОТКРЫТЬ КОШЕЛЕК",
receive: "ПОЛУЧИТЬ",
registerServiceNode: "ЗАРЕГИСТРИРОВАТЬ СЕРВИСНУЮ НОДУ",
rescan: "ПЕРЕСКАНИРОВАТЬ",
restoreWallet: "ВОССТАНОВИТЬ КОШЕЛЕК",
save: "СОХРАНИТЬ",
saveTxNotes: "СОХРАНИТЬ ТРАНЗ. ЗАМЕТКИ",
selectLocation: "ВЫБРАТЬ ПАПКУ",
selectWalletFile: "ВЫБРАТЬ ФАЙЛ КОШЕЛЬКА",
send: "ОТПРАВИТЬ",
sendCoins: "ОТПРАВИТЬ МОНЕТЫ",
serviceNode: "СЕРВИСНАЯ НОДА",
settings: "НАСТРОЙКИ",
showQRCode: "ПОКАЗАТЬ QR КОД",
showTxDetails: "ПОКАЗАТЬ ДЕТАЛИ ТРАНЗАКЦИИ",
stake: "СТЕЙК",
sweepAll: "SWEEP ALL",
unlock: "UNLOCK",
viewOnExplorer: "ПОСМОТРЕТЬ В ЭКСПЛОРЕРЕ"
},
dialog: {
// Generic buttons
buttons: {
// All button text is uppercased in the gui
advanced: "ADVANCED",
all: "ВСЕ",
back: "НАЗАД",
browse: "ОБЗОР",
cancel: "ОТМЕНА",
change: "ИЗМЕНИТЬ",
check: "CHECK",
clear: "CLEAR",
close: "ЗАКРЫТЬ",
contacts: "КОНТАКТЫ",
copyAddress: "КОПИРОВАТЬ АДРЕС",
copySignature: "COPY SIGNATURE",
createWallet: "СОЗДАТЬ КОШЕЛЕК",
delete: "УДАЛИТЬ",
edit: "РЕДАКТИРОВАТЬ",
export: "ЭКСПОРТИРОВАТЬ",
generate: "GENERATE",
import: "ИМПОРТИРОВАТЬ",
importWallet: "ИМПОРТИРОВАТЬ КОШЕЛЕК | ИМПОРТИРОВАТЬ КОШЕЛЬКИ",
next: "ДАЛЕЕ",
openWallet: "ОТКРЫТЬ КОШЕЛЕК",
receive: "ПОЛУЧИТЬ",
registerServiceNode: "ЗАРЕГИСТРИРОВАТЬ СЕРВИСНУЮ НОДУ",
rescan: "ПЕРЕСКАНИРОВАТЬ",
restoreWallet: "ВОССТАНОВИТЬ КОШЕЛЕК",
save: "СОХРАНИТЬ",
saveTxNotes: "СОХРАНИТЬ ТРАНЗ. ЗАМЕТКИ",
selectLocation: "ВЫБРАТЬ ПАПКУ",
selectWalletFile: "ВЫБРАТЬ ФАЙЛ КОШЕЛЬКА",
send: "ОТПРАВИТЬ",
sendCoins: "ОТПРАВИТЬ МОНЕТЫ",
serviceNode: "СЕРВИСНАЯ НОДА",
settings: "НАСТРОЙКИ",
showQRCode: "ПОКАЗАТЬ QR КОД",
showTxDetails: "ПОКАЗАТЬ ДЕТАЛИ ТРАНЗАКЦИИ",
stake: "СТЕЙК",
sweepAll: "SWEEP ALL",
unlock: "UNLOCK",
viewOnExplorer: "ПОСМОТРЕТЬ В ЭКСПЛОРЕРЕ"
ok: "ОК",
cancel: "ОТМЕНИТЬ",
open: "ОТКРЫТЬ"
},
dialog: {
// Generic buttons
buttons: {
ok: "ОК",
cancel: "ОТМЕНИТЬ",
open: "ОТКРЫТЬ"
},
// Dialogs
banPeer: {
title: "Забанить пира",
peerDetailsTitle: "Данные пира",
message: "Введите время, на которое нужно забанить пира (сек).\nПо-умолчанию 3600 = 1 час.",
ok: "Забанить"
},
copyAddress: {
title: "Копировать адрес",
message: "С этим адресом ассоциирован payment id.\nPayment id необходимо скопировать отдельно."
},
copyPrivateKeys: {
// Copy {seedWords/viewKey/spendKey}
title: "Скопировать: {type}",
message: "Будьте внимательны, владелец приватных ключей может контролировать ваши средства.",
seedWords: "Seed-фраза",
viewKey: "Ключ Просмотра",
spendKey: "Ключ Отправки"
},
deleteWallet: {
title: "Удалить кошелек",
message: "Уверены, что хотите удалить кошелек?\nУбедитесь, что сохранили приватные ключи в надежном месте.\nЭТО НЕОБРАТИМЫЙ ПРОЦЕСС!",
ok: "УДАЛИТЬ"
},
exit: {
title: "Выйти",
message: "Уверены, что хотите выйти?",
ok: "ВЫЙТИ"
},
keyImages: {
title: "{type} ключевых образов",
message: "Вы уверены, что хотите произвести {type} ключевых образов?",
export: "Экспорт",
import: "Импорт"
},
noPassword: {
title: "Пароль не задан",
message: "Вы уверены, что хотите создать кошелек без пароля?",
ok: "СОЗДАТЬ"
},
password: {
title: "Пароль",
message: "Введите пароль кошелька"
},
registerServiceNode: {
title: "Регистрация сервисной ноды",
message: "Вы уверены, что хотите зарегистрировать сервисную ноду?",
ok: "ЗАРЕГИСТРИРОВАТЬ"
},
rescan: {
title: "Пересканировать кошелек",
message: "Внимание: Часть информации о предыдущих тразакциях\n(например, адрес получателя) будет потеряна.",
ok: "ПЕРЕСКАНИРОВАТЬ"
},
restart: {
title: "ПЕРЕЗАГРУЗИТЬ",
message: "Изменения требуют перезагрузки. Выполнить сейчас?",
ok: "ПЕРЕЗАГРУЗИТЬ"
},
showPrivateKeys: {
title: "Показать приватные ключи",
message: "Вы уверены, что хотите посмотреть приватные ключи?",
ok: "ПОКАЗАТЬ"
},
stake: {
title: "Стейк",
message: "Вы уверены, что хотите начать стейк ноды?",
ok: "ПРОДОЛЖИТЬ"
},
sweepAll: {
title: "Sweep all",
message: "Do you want to sweep all?",
ok: "SWEEP ALL"
},
sweepAllWarning: {
title: "Sweep all warning",
message: "You are about to combine all of your unspent funds by sending a transaction to yourself, your wallet may show a balance of 0 temporarily, after 10 blocks your funds will unlock and you may stake normally.",
ok: "CONTINUE"
},
switchWallet: {
title: "Переключить кошельки",
closeMessage: "Вы уверены, что хотите закрыть текущий кошелек?",
restartMessage: "Запущен RPC кошелька.\nЕсли вы хотите переключить кошельки, перезапустите приложение. \nПрогресс синхронизации будет потерян и вам придется пересканировать блокчейн."
},
transactionDetails: {
title: "Детали транзакции",
ok: "ЗАКРЫТЬ"
},
transfer: {
title: "Перевод",
message: "Вы уверены, что хотите отправить эту транзакцию?",
ok: "ОТПАРВИТЬ"
},
unlockConfirm: {
title: "Подтвердить разблокировку",
ok: "РАЗБЛОКИРОВАТЬ"
},
unlockServiceNode: {
title: "Разблокировать сервисную ноду",
confirmTitle: "Подтверждение разблокировки",
message: "Вы уверены, что хотите разблокировать сервисную ноду?",
ok: "РАЗБЛОКИРОВАТЬ"
},
unlockServiceNodeWarning: {
title: "Unlock service node warning",
message: "Unlocking a partial stake in a node will also unstake for any other participants, if staking in a shared node its best to let the operator and other participants know you are unstaking.",
ok: "CONTINUE"
}
// Dialogs
banPeer: {
title: "Забанить пира",
peerDetailsTitle: "Данные пира",
message: "Введите время, на которое нужно забанить пира (сек).\nПо-умолчанию 3600 = 1 час.",
ok: "Забанить"
},
fieldLabels: {
// Field labels are also all uppercased
address: "АДРЕС",
amount: "СУММА",
confirmPassword: "ПОДТВЕРЖДЕНИЕ ПАРОЛЯ",
daemonLogLevel: "УРОВЕНЬ ЛОГА ДЕМОНА",
daemonP2pPort: "P2P ПОРТ ДЕМОНА",
daemonZMQPort: "ZMQ ПОРТ ДЕМОНА",
dataStoragePath: "ПАПКА ХРАНИЛИЩА ДАННЫХ",
filter: "FILTER",
filterTransactionType: "ТИП ТРАНЗАКЦИИ",
internalWalletPort: "ВНУТРЕННЫЙ ПОРТ КОШЕЛЬКА",
keyImages: {
exportDirectory: "ПАПКА ЭКСПОРТА КЛЮЧЕВОГО ОБРАЗА",
importFile: "ФАЙЛ ИМПОРТА КЛЮЧЕВОГО ОБРАЗА"
},
limitDownloadRate: "ОГРАНИЧИТЬ СКОРОСТЬ ЗАГРУЗКИ",
limitUploadRate: "ОГРАНИЧИТЬ СКОРОСТЬ АПЛОАДА",
localDaemonIP: "IP ЛОКАЛЬНОГО ДЕМОНА",
localDaemonPort: "ПОРТ ЛОКАЛЬНОГО ДЕМОНА",
maxIncomingPeers: "МАКСИМУМ ВХОДЯЩИХ ПИРОВ",
maxOutgoingPeers: "МАКСИМУМ ИСХОДЯЩИХ ПИРОВ",
message: "MESSAGE",
mnemonicSeed: "МНЕМОНИЧЕСКАЯ SEED-ФРАЗА",
name: "ИМЯ",
newWalletName: "ИМЯ НОВОГО КОШЕЛЬКА",
notes: "ЗАМЕТКИ",
optional: "НЕОБЯЗАТЕЛЬНО",
password: "ПАРОЛЬ",
paymentId: "PAYMENT ID",
priority: "ПРИОРИТЕТ",
remoteNodeHost: "ХОСТ УДАЛЕННОЙ НОДЫ",
remoteNodePort: "ПОРТ УДАЛЕННОЙ НОДЫ",
restoreFromBlockHeight: "ВОССТАНОВИТЬ НАЧИНАЯ С ВЫСОТЫ БЛОКА",
restoreFromDate: "ВОССТАНОВИТЬ НАЧИНАЯ С ДАТЫ",
seedLanguage: "ЯЗЫК SEED-ФРАЗЫ",
serviceNodeCommand: "КОМАНДА РЕГИСТРАЦИИ СЕРВИСНОЙ НОДЫ",
serviceNodeKey: "КЛЮЧ СЕРВИСНОЙ НОДЫ",
signature: "SIGNATURE",
transactionId: "TRANSACTION ID",
walletFile: "ФАЙЛ КОШЕЛЬКА",
walletLogLevel: "УРОВЕНЬ ЛОГА КОШЕЛЬКА",
walletName: "ИМЯ КОШЕЛЬКА",
walletRPCPort: "RPC ПОРТ КОШЕЛЬКА",
walletStoragePath: "ПАПКА КОШЕЛЬКА",
// These are specific labels which do not get uppercased
confirmNewPassword: "Подтвердите Новый Пароль",
newPassword: "Новый Пароль",
oldPassword: "Старый Пароль",
rescanFullBlockchain: "Пересканировать весь блокчейн",
rescanSpentOutputs: "Пересканировать потраченные выходы",
transactionNotes: "Заметки Транзакции",
chooseNetwork: "Выберите сеть",
network: "Сеть"
copyAddress: {
title: "Копировать адрес",
message: "С этим адресом ассоциирован payment id.\nPayment id необходимо скопировать отдельно."
},
footer: {
ready: "СИНХРОНИЗИРОВАН",
scanning: "СКАНИРОВАНИЕ",
status: "Статус",
syncing: "СИНХРОНИЗАЦИЯ",
remote: "Удаленная нода",
wallet: "Кошелек"
copyPrivateKeys: {
// Copy {seedWords/viewKey/spendKey}
title: "Скопировать: {type}",
message: "Будьте внимательны, владелец приватных ключей может контролировать ваши средства.",
seedWords: "Seed-фраза",
viewKey: "Ключ Просмотра",
spendKey: "Ключ Отправки"
},
menuItems: {
about: "Информация",
changePassword: "Сменить Пароль",
copyAddress: "Копировать адрес",
copyQR: "Копировать QR код",
copySeedWords: "Копировать seed-фразу",
copySpendKey: "Копировать Ключ Отправки",
copyServiceNodeKey: "Copy service node key",
copyTransactionId: "Копировать ID транзакции",
copyViewKey: "Копировать Ключ Просмотра",
createNewWallet: "Создать новый кошелек",
deleteWallet: "Удалить Кошелек",
exit: "Закрыть Кошелек Loki",
importOldGUIWallet: "Импортировать кошельки из старого GUI",
manageKeyImages: "Управлять Ключевыми Образами",
openWallet: "Открыть кошелек",
rescanWallet: "Пересканировать кошелек",
restoreWalletFile: "Восстановить кошелек из файла",
restoreWalletSeed: "Восстановить кошелек из seed-фразы",
saveQR: "Сохранить QR код в файл",
sendToThisAddress: "Отправить на этот адрес",
settings: "Настройки",
showDetails: "Показать подробности",
showPrivateKeys: "Показать приватные ключи",
showQRCode: "Показать QR Код",
switchWallet: "Переключить Кошелек",
viewOnExplorer: "Посмотреть в обозревателе блокчейна"
deleteWallet: {
title: "Удалить кошелек",
message:
"Уверены, что хотите удалить кошелек?\nУбедитесь, что сохранили приватные ключи в надежном месте.\nЭТО НЕОБРАТИМЫЙ ПРОЦЕСС!",
ok: "УДАЛИТЬ"
},
notification: {
positive: {
addressCopied: "Адрес скопирован в буфере",
bannedPeer: "{host} забанен до {time}",
copied: "{item} скопирован в буфер",
itemSaved: "{item} сохранен в {filename}",
keyImages: {
exported: "Ключевые образы экспортированы в {filename}",
imported: "Ключевые образы импортированы"
},
passwordUpdated: "Пароль обновлен",
qrCopied: "QR код скопирован в буфер",
registerServiceNodeSuccess: "Сервисная нода успешно зарегистрирована",
sendSuccess: "Транзакция успешно отправлена",
signatureCopied: "Signature copied to clipboard",
stakeSuccess: "Стейк успешно начат",
transactionNotesSaved: "Заметка о транзакции сохранена"
},
errors: {
banningPeer: "Ошибка бана пира",
cannotAccessRemoteNode: "Не удалось подключиться к удаленной ноде, пожалуйста выберите другую ноду",
changingPassword: "Ошибка смены пароля",
copyWalletFail: "Не удалось скопировать кошелек",
copyingPrivateKeys: "Не удалось скопировать приватные ключи",
dataPathNotFound: "Папка хранилища данных не найдена",
differentNetType: "Удаленная нода использует другой тип сети (nettype)",
enterSeedWords: "Введите seed-фразу",
enterTransactionId: "Enter transaction ID",
enterTransactionProof: "Enter transaction proof",
enterWalletName: "Введите имя кошелька",
errorSavingItem: "Ошибка сохранения {item}",
failedServiceNodeUnlock: "Не удалось разблокировать сервисную ноду",
failedToSetLanguage: "Не удалось переключить язык: {lang}",
failedWalletImport: "Не удалось импортировать кошелек",
failedWalletOpen: "Не удалось открыть кошелек. Попробуйте еще раз.",
internalError: "Внутренняя ошибка",
invalidAddress: "Адрес не верен",
invalidAmount: "Сумма не верна",
invalidOldPassword: "Старый пароль не верен",
invalidPassword: "Неверный пароль",
invalidPaymentId: "Payment id не верен",
invalidPrivateViewKey: "Неверный приватный ключ просмотра",
invalidPublicAddress: "Неверный публичный адрес",
invalidRestoreDate: "Неверная дата восстановления",
invalidRestoreHeight: "Неверная высота блока восстановления",
invalidSeedLength: "Неверная длина seed-фразы",
invalidServiceNodeCommand: "Пожалуйста введите команду регистрации сервисной ноды",
invalidServiceNodeKey: "Ключ сервисной ноды указан неверно",
invalidWalletPath: "Неверный путь к кошельку",
keyImages: {
exporting: "Ошибка экспорта ключевых образов",
reading: "Ошибка чтения ключевых образов",
importing: "Ошибка импорта ключевых образов"
},
negativeAmount: "Сумма не может быть отрицательной",
newPasswordNoMatch: "Новые пароли не совпадают",
newPasswordSame: "Новый пароль должен отличаться от старого",
notEnoughBalance: "Разблокированный баланс недостаточен",
passwordNoMatch: "Пароли не совпадают",
remoteCannotBeReached: "Не удалось связаться с удаленным демоном",
selectWalletFile: "Select a wallet file",
unknownError: "Произошла неизвестная ошибка",
walletAlreadyExists: "Кошелек с таким именем уже существует",
walletPathNotFound: "Путь к хранилищу данных кошелька не найден",
zeroAmount: "Сумма должна быть больше нуля"
},
warnings: {
noKeyImageExport: "Не найдено ключевых образов для экспорта",
usingLocalNode: "Не удалось подключиться к удаленной ноде, переключаемся на локальную ноду",
usingRemoteNode: "Не найден файл lokid, используется удаленная нода"
}
exit: {
title: "Выйти",
message: "Уверены, что хотите выйти?",
ok: "ВЫЙТИ"
},
placeholders: {
additionalNotes: "Дополнительные заметки",
addressBookName: "Имя для этого адреса",
filterTx: "Enter an ID, name, address or amount",
hexCharacters: "Шестнадцатеричных символов: {count}",
mnemonicSeed: "Seed-фраза, состоящая из 25 или 24 слов",
pasteTransactionId: "Paste transaction ID",
pasteTransactionProof: "Paste transaction proof",
proveOptionalMessage: "Optional message against which the signature is signed",
recipientWalletAddress: "Recipient's wallet address",
selectAFile: "Пожалуйста, выберите файл",
transactionNotes: "Дополнительные заметки для приосединения к транзакции",
walletName: "Имя вашего кошелька",
walletPassword: "Пароль кошелька (необязательно)"
keyImages: {
title: "{type} ключевых образов",
message: "Вы уверены, что хотите произвести {type} ключевых образов?",
export: "Экспорт",
import: "Импорт"
},
strings: {
addAddressBookEntry: "Добавить запись в адресную книгу",
addressBookDetails: "Детальные данные адресной книги",
addressBookIsEmpty: "Адресная книга пуста",
addresses: {
myPrimaryAddress: "Мой основной адрес",
myUnusedAddresses: "Мои неиспользованные адреса",
myUsedAddresses: "Мои использованные адреса",
primaryAddress: "Основной адрес",
subAddress: "Суб-адрес",
subAddressIndex: "Индекс {index}"
},
advancedOptions: "Расширенные настройки",
bannedPeers: {
title: "Забаненные пиры (баны будут сняты при перезагрузке кошелька)",
bannedUntil: "Забанено до {time}"
},
blockHeight: "Высота блока",
checkTransaction: {
description: "Verify that funds were paid to an address by supplying the transaction ID, the recipient address, the message used for signing and the signature.\nFor a 'Spend Proof' you dont need to provide the recipient address.",
infoTitles: {
confirmations: "Confirmations",
inPool: "In pool",
validTransaction: "Valid transaction",
received: "Received amount"
},
validTransaction: {
no: "NO",
yes: "YES"
}
},
closing: "Закрытие",
connectingToBackend: "Подключение...",
contribution: "Contribution",
daemon: {
local: {
title: "Только Локальная Нода",
description: "Полная безопасность, кошелек загрузит весь блокчейн. Вы не сможете совершать платежи до завершения синхронизации."
},
localRemote: {
title: "Локальная + Удаленная Нода",
description: "Начните рабоать с кошельком быстрее, используя эту опцию (по-умолчанию). Кошелек загрузит весь блокчейн, но до окончания синхронизации будет использовать удаленную ноду."
},
remote: {
title: "Только Удаленная Нода",
description: "Меньшая безопасность, кошелек подключится к удаленной ноде для проведения всех транзакций."
}
},
destinationUnknown: "Назначение Неизвестно",
editAddressBookEntry: "Редактировать запись адресной книги",
loadingSettings: "Загрузка настроек",
lokiBalance: "Баланс",
lokiUnlockedBalance: "Разблокированый баланс",
lokiUnlockedShort: "Разблокировано",
noTransactionsFound: "Транзакции не найдены",
notes: "Заметки",
numberOfUnspentOutputs: "Количество непотраченных выходов",
paymentID: "Payment ID",
peerList: "Список пиров",
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.",
priorityOptions: {
automatic: "Автоматически",
slow: "Медленно",
normal: "Нормально",
fast: "Быстро",
fastest: "Очень быстро"
},
readingWalletList: "Получение списка кошельков",
recentIncomingTransactionsToAddress: "Недавние входящие транзакции на этот адрес",
recentTransactionsWithAddress: "Недавние транзакции с этим адресом",
rescanModalDescription: "Выберите полное пересканирование или персканирование только потраченных выходов.",
saveSeedWarning: "Пожалуйста, скопируйте фразу и храните ее в безопасном месте!",
saveToAddressBook: "Сохранить в адресной книге",
seedWords: "Seed-фраза",
selectLanguage: "Выберите язык",
serviceNodeRegistrationDescription: "Введите команду {registerCommand}, созданную демоном при регистрации Сервисной Ноды с помощью команды \"{prepareCommand}\"",
spendKey: "Ключ Отправки",
startingDaemon: "Запуск демона",
startingWallet: "Запуск кошелька",
switchToDateSelect: "Переключиться на выбор даты",
switchToHeightSelect: "Переключиться на выбор высоты блока",
transactionID: "ID транзакции",
transactionConfirmed: "подтверждено",
transactions: {
amount: "Сумма",
description: "Транзакция: {type}",
fee: "Комиссия",
paidBySender: "оплачивается отправителем",
received: "Получено",
sent: "Отправлено",
sentTo: "Транзакция отправлена: {type}",
timestamp: "Отметка времени",
types: {
all: "Все",
incoming: "Входящие",
outgoing: "Исходящие",
pending: "Ожидающие",
pendingIncoming: "Ожидающие входящие",
pendingOutgoing: "Ожидающие исходящие",
miner: "Майнинг",
serviceNode: "Сервисная Нода",
governance: "Управление",
stake: "Стейк",
failed: "Не удавшиеся"
}
},
unspentOutputs: "Непотраченные выходы",
userNotUsedAddress: "Вы не использовали этот адрес",
userUsedAddress: "Вы использовали этот адрес",
viewKey: "Ключ Просмотра",
viewOnlyMode: "Режим просмотра. Пожалуйста загрузите полный кошелек, чтобы отправлять монеты."
noPassword: {
title: "Пароль не задан",
message: "Вы уверены, что хотите создать кошелек без пароля?",
ok: "СОЗДАТЬ"
},
titles: {
addressBook: "Адресная книга",
addressDetails: "Данные адреса",
advanced: {
checkTransaction: "CHECK TRANSACTION",
prove: "PROVE"
},
changePassword: "Сменить пароль",
configure: "Настройки",
currentlyStakedNodes: "Currently staked nodes",
privateKeys: "Приватные ключи",
rescanWallet: "Пересканировать кошелек",
serviceNode: {
registration: "РЕГИСТРАЦИЯ",
staking: "СТЕЙК"
},
settings: {
title: "Настройки",
tabs: {
general: "Общие",
language: "Язык",
peers: "Пиры"
}
},
transactionDetails: "Данные транзакции",
transactions: "Транзакции",
wallet: {
createNew: "Создать новый кошелек",
createdOrRestored: "Кошелек создан/восстановлен",
importFromFile: "Импортировать кошелек из файла",
importFromLegacyGUI: "Импортировать кошелек из предыдущего GUI",
importFromOldGUI: "Импортировать кошелек из старого GUI",
restoreFromSeed: "Восстановить кошелек из seed-фразы",
restoreViewOnly: "Восстановить кошелек только-для-чтения"
},
welcome: "Добро пожаловать",
yourWallets: "Ваши Кошельки"
password: {
title: "Пароль",
message: "Введите пароль кошелька"
},
registerServiceNode: {
title: "Регистрация сервисной ноды",
message: "Вы уверены, что хотите зарегистрировать сервисную ноду?",
ok: "ЗАРЕГИСТРИРОВАТЬ"
},
rescan: {
title: "Пересканировать кошелек",
message: "Внимание: Часть информации о предыдущих тразакциях\n(например, адрес получателя) будет потеряна.",
ok: "ПЕРЕСКАНИРОВАТЬ"
},
restart: {
title: "ПЕРЕЗАГРУЗИТЬ",
message: "Изменения требуют перезагрузки. Выполнить сейчас?",
ok: "ПЕРЕЗАГРУЗИТЬ"
},
showPrivateKeys: {
title: "Показать приватные ключи",
message: "Вы уверены, что хотите посмотреть приватные ключи?",
ok: "ПОКАЗАТЬ"
},
stake: {
title: "Стейк",
message: "Вы уверены, что хотите начать стейк ноды?",
ok: "ПРОДОЛЖИТЬ"
},
sweepAll: {
title: "Sweep all",
message: "Do you want to sweep all?",
ok: "SWEEP ALL"
},
sweepAllWarning: {
title: "Sweep all warning",
message:
"You are about to combine all of your unspent funds by sending a transaction to yourself, your wallet may show a balance of 0 temporarily, after 10 blocks your funds will unlock and you may stake normally.",
ok: "CONTINUE"
},
switchWallet: {
title: "Переключить кошельки",
closeMessage: "Вы уверены, что хотите закрыть текущий кошелек?",
restartMessage:
"Запущен RPC кошелька.\nЕсли вы хотите переключить кошельки, перезапустите приложение. \nПрогресс синхронизации будет потерян и вам придется пересканировать блокчейн."
},
transactionDetails: {
title: "Детали транзакции",
ok: "ЗАКРЫТЬ"
},
transfer: {
title: "Перевод",
message: "Вы уверены, что хотите отправить эту транзакцию?",
ok: "ОТПАРВИТЬ"
},
unlockConfirm: {
title: "Подтвердить разблокировку",
ok: "РАЗБЛОКИРОВАТЬ"
},
unlockServiceNode: {
title: "Разблокировать сервисную ноду",
confirmTitle: "Подтверждение разблокировки",
message: "Вы уверены, что хотите разблокировать сервисную ноду?",
ok: "РАЗБЛОКИРОВАТЬ"
},
unlockServiceNodeWarning: {
title: "Unlock service node warning",
message:
"Unlocking a partial stake in a node will also unstake for any other participants, if staking in a shared node its best to let the operator and other participants know you are unstaking.",
ok: "CONTINUE"
}
}
},
fieldLabels: {
// Field labels are also all uppercased
address: "АДРЕС",
amount: "СУММА",
confirmPassword: "ПОДТВЕРЖДЕНИЕ ПАРОЛЯ",
daemonLogLevel: "УРОВЕНЬ ЛОГА ДЕМОНА",
daemonP2pPort: "P2P ПОРТ ДЕМОНА",
daemonZMQPort: "ZMQ ПОРТ ДЕМОНА",
dataStoragePath: "ПАПКА ХРАНИЛИЩА ДАННЫХ",
filter: "FILTER",
filterTransactionType: "ТИП ТРАНЗАКЦИИ",
internalWalletPort: "ВНУТРЕННЫЙ ПОРТ КОШЕЛЬКА",
keyImages: {
exportDirectory: "ПАПКА ЭКСПОРТА КЛЮЧЕВОГО ОБРАЗА",
importFile: "ФАЙЛ ИМПОРТА КЛЮЧЕВОГО ОБРАЗА"
},
limitDownloadRate: "ОГРАНИЧИТЬ СКОРОСТЬ ЗАГРУЗКИ",
limitUploadRate: "ОГРАНИЧИТЬ СКОРОСТЬ АПЛОАДА",
localDaemonIP: "IP ЛОКАЛЬНОГО ДЕМОНА",
localDaemonPort: "ПОРТ ЛОКАЛЬНОГО ДЕМОНА",
maxIncomingPeers: "МАКСИМУМ ВХОДЯЩИХ ПИРОВ",
maxOutgoingPeers: "МАКСИМУМ ИСХОДЯЩИХ ПИРОВ",
message: "MESSAGE",
mnemonicSeed: "МНЕМОНИЧЕСКАЯ SEED-ФРАЗА",
name: "ИМЯ",
newWalletName: "ИМЯ НОВОГО КОШЕЛЬКА",
notes: "ЗАМЕТКИ",
optional: "НЕОБЯЗАТЕЛЬНО",
password: "ПАРОЛЬ",
paymentId: "PAYMENT ID",
priority: "ПРИОРИТЕТ",
remoteNodeHost: "ХОСТ УДАЛЕННОЙ НОДЫ",
remoteNodePort: "ПОРТ УДАЛЕННОЙ НОДЫ",
restoreFromBlockHeight: "ВОССТАНОВИТЬ НАЧИНАЯ С ВЫСОТЫ БЛОКА",
restoreFromDate: "ВОССТАНОВИТЬ НАЧИНАЯ С ДАТЫ",
seedLanguage: "ЯЗЫК SEED-ФРАЗЫ",
serviceNodeCommand: "КОМАНДА РЕГИСТРАЦИИ СЕРВИСНОЙ НОДЫ",
serviceNodeKey: "КЛЮЧ СЕРВИСНОЙ НОДЫ",
signature: "SIGNATURE",
transactionId: "TRANSACTION ID",
walletFile: "ФАЙЛ КОШЕЛЬКА",
walletLogLevel: "УРОВЕНЬ ЛОГА КОШЕЛЬКА",
walletName: "ИМЯ КОШЕЛЬКА",
walletRPCPort: "RPC ПОРТ КОШЕЛЬКА",
walletStoragePath: "ПАПКА КОШЕЛЬКА",
// These are specific labels which do not get uppercased
confirmNewPassword: "Подтвердите Новый Пароль",
newPassword: "Новый Пароль",
oldPassword: "Старый Пароль",
rescanFullBlockchain: "Пересканировать весь блокчейн",
rescanSpentOutputs: "Пересканировать потраченные выходы",
transactionNotes: "Заметки Транзакции",
chooseNetwork: "Выберите сеть",
network: "Сеть"
},
footer: {
ready: "СИНХРОНИЗИРОВАН",
scanning: "СКАНИРОВАНИЕ",
status: "Статус",
syncing: "СИНХРОНИЗАЦИЯ",
remote: "Удаленная нода",
wallet: "Кошелек"
},
menuItems: {
about: "Информация",
changePassword: "Сменить Пароль",
copyAddress: "Копировать адрес",
copyQR: "Копировать QR код",
copySeedWords: "Копировать seed-фразу",
copySpendKey: "Копировать Ключ Отправки",
copyServiceNodeKey: "Copy service node key",
copyTransactionId: "Копировать ID транзакции",
copyViewKey: "Копировать Ключ Просмотра",
createNewWallet: "Создать новый кошелек",
deleteWallet: "Удалить Кошелек",
exit: "Закрыть Кошелек Loki",
importOldGUIWallet: "Импортировать кошельки из старого GUI",
manageKeyImages: "Управлять Ключевыми Образами",
openWallet: "Открыть кошелек",
rescanWallet: "Пересканировать кошелек",
restoreWalletFile: "Восстановить кошелек из файла",
restoreWalletSeed: "Восстановить кошелек из seed-фразы",
saveQR: "Сохранить QR код в файл",
sendToThisAddress: "Отправить на этот адрес",
settings: "Настройки",
showDetails: "Показать подробности",
showPrivateKeys: "Показать приватные ключи",
showQRCode: "Показать QR Код",
switchWallet: "Переключить Кошелек",
viewOnExplorer: "Посмотреть в обозревателе блокчейна"
},
notification: {
positive: {
addressCopied: "Адрес скопирован в буфере",
bannedPeer: "{host} забанен до {time}",
copied: "{item} скопирован в буфер",
itemSaved: "{item} сохранен в {filename}",
keyImages: {
exported: "Ключевые образы экспортированы в {filename}",
imported: "Ключевые образы импортированы"
},
passwordUpdated: "Пароль обновлен",
qrCopied: "QR код скопирован в буфер",
registerServiceNodeSuccess: "Сервисная нода успешно зарегистрирована",
sendSuccess: "Транзакция успешно отправлена",
signatureCopied: "Signature copied to clipboard",
stakeSuccess: "Стейк успешно начат",
transactionNotesSaved: "Заметка о транзакции сохранена"
},
errors: {
banningPeer: "Ошибка бана пира",
cannotAccessRemoteNode: "Не удалось подключиться к удаленной ноде, пожалуйста выберите другую ноду",
changingPassword: "Ошибка смены пароля",
copyWalletFail: "Не удалось скопировать кошелек",
copyingPrivateKeys: "Не удалось скопировать приватные ключи",
dataPathNotFound: "Папка хранилища данных не найдена",
differentNetType: "Удаленная нода использует другой тип сети (nettype)",
enterSeedWords: "Введите seed-фразу",
enterTransactionId: "Enter transaction ID",
enterTransactionProof: "Enter transaction proof",
enterWalletName: "Введите имя кошелька",
errorSavingItem: "Ошибка сохранения {item}",
failedServiceNodeUnlock: "Не удалось разблокировать сервисную ноду",
failedToSetLanguage: "Не удалось переключить язык: {lang}",
failedWalletImport: "Не удалось импортировать кошелек",
failedWalletOpen: "Не удалось открыть кошелек. Попробуйте еще раз.",
internalError: "Внутренняя ошибка",
invalidAddress: "Адрес не верен",
invalidAmount: "Сумма не верна",
invalidOldPassword: "Старый пароль не верен",
invalidPassword: "Неверный пароль",
invalidPaymentId: "Payment id не верен",
invalidPrivateViewKey: "Неверный приватный ключ просмотра",
invalidPublicAddress: "Неверный публичный адрес",
invalidRestoreDate: "Неверная дата восстановления",
invalidRestoreHeight: "Неверная высота блока восстановления",
invalidSeedLength: "Неверная длина seed-фразы",
invalidServiceNodeCommand: "Пожалуйста введите команду регистрации сервисной ноды",
invalidServiceNodeKey: "Ключ сервисной ноды указан неверно",
invalidWalletPath: "Неверный путь к кошельку",
keyImages: {
exporting: "Ошибка экспорта ключевых образов",
reading: "Ошибка чтения ключевых образов",
importing: "Ошибка импорта ключевых образов"
},
negativeAmount: "Сумма не может быть отрицательной",
newPasswordNoMatch: "Новые пароли не совпадают",
newPasswordSame: "Новый пароль должен отличаться от старого",
notEnoughBalance: "Разблокированный баланс недостаточен",
passwordNoMatch: "Пароли не совпадают",
remoteCannotBeReached: "Не удалось связаться с удаленным демоном",
selectWalletFile: "Select a wallet file",
unknownError: "Произошла неизвестная ошибка",
walletAlreadyExists: "Кошелек с таким именем уже существует",
walletPathNotFound: "Путь к хранилищу данных кошелька не найден",
zeroAmount: "Сумма должна быть больше нуля"
},
warnings: {
noKeyImageExport: "Не найдено ключевых образов для экспорта",
usingLocalNode: "Не удалось подключиться к удаленной ноде, переключаемся на локальную ноду",
usingRemoteNode: "Не найден файл lokid, используется удаленная нода"
}
},
placeholders: {
additionalNotes: "Дополнительные заметки",
addressBookName: "Имя для этого адреса",
filterTx: "Enter an ID, name, address or amount",
hexCharacters: "Шестнадцатеричных символов: {count}",
mnemonicSeed: "Seed-фраза, состоящая из 25 или 24 слов",
pasteTransactionId: "Paste transaction ID",
pasteTransactionProof: "Paste transaction proof",
proveOptionalMessage: "Optional message against which the signature is signed",
recipientWalletAddress: "Recipient's wallet address",
selectAFile: "Пожалуйста, выберите файл",
transactionNotes: "Дополнительные заметки для приосединения к транзакции",
walletName: "Имя вашего кошелька",
walletPassword: "Пароль кошелька (необязательно)"
},
strings: {
addAddressBookEntry: "Добавить запись в адресную книгу",
addressBookDetails: "Детальные данные адресной книги",
addressBookIsEmpty: "Адресная книга пуста",
addresses: {
myPrimaryAddress: "Мой основной адрес",
myUnusedAddresses: "Мои неиспользованные адреса",
myUsedAddresses: "Мои использованные адреса",
primaryAddress: "Основной адрес",
subAddress: "Суб-адрес",
subAddressIndex: "Индекс {index}"
},
advancedOptions: "Расширенные настройки",
bannedPeers: {
title: "Забаненные пиры (баны будут сняты при перезагрузке кошелька)",
bannedUntil: "Забанено до {time}"
},
blockHeight: "Высота блока",
checkTransaction: {
description:
"Verify that funds were paid to an address by supplying the transaction ID, the recipient address, the message used for signing and the signature.\nFor a 'Spend Proof' you dont need to provide the recipient address.",
infoTitles: {
confirmations: "Confirmations",
inPool: "In pool",
validTransaction: "Valid transaction",
received: "Received amount"
},
validTransaction: {
no: "NO",
yes: "YES"
}
},
closing: "Закрытие",
connectingToBackend: "Подключение...",
contribution: "Contribution",
daemon: {
local: {
title: "Только Локальная Нода",
description:
"Полная безопасность, кошелек загрузит весь блокчейн. Вы не сможете совершать платежи до завершения синхронизации."
},
localRemote: {
title: "Локальная + Удаленная Нода",
description:
"Начните рабоать с кошельком быстрее, используя эту опцию (по-умолчанию). Кошелек загрузит весь блокчейн, но до окончания синхронизации будет использовать удаленную ноду."
},
remote: {
title: "Только Удаленная Нода",
description: "Меньшая безопасность, кошелек подключится к удаленной ноде для проведения всех транзакций."
}
},
destinationUnknown: "Назначение Неизвестно",
editAddressBookEntry: "Редактировать запись адресной книги",
loadingSettings: "Загрузка настроек",
lokiBalance: "Баланс",
lokiUnlockedBalance: "Разблокированый баланс",
lokiUnlockedShort: "Разблокировано",
noTransactionsFound: "Транзакции не найдены",
notes: "Заметки",
numberOfUnspentOutputs: "Количество непотраченных выходов",
paymentID: "Payment ID",
peerList: "Список пиров",
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.",
priorityOptions: {
automatic: "Автоматически",
slow: "Медленно",
normal: "Нормально",
fast: "Быстро",
fastest: "Очень быстро"
},
readingWalletList: "Получение списка кошельков",
recentIncomingTransactionsToAddress: "Недавние входящие транзакции на этот адрес",
recentTransactionsWithAddress: "Недавние транзакции с этим адресом",
rescanModalDescription: "Выберите полное пересканирование или персканирование только потраченных выходов.",
saveSeedWarning: "Пожалуйста, скопируйте фразу и храните ее в безопасном месте!",
saveToAddressBook: "Сохранить в адресной книге",
seedWords: "Seed-фраза",
selectLanguage: "Выберите язык",
serviceNodeRegistrationDescription:
'Введите команду {registerCommand}, созданную демоном при регистрации Сервисной Ноды с помощью команды "{prepareCommand}"',
spendKey: "Ключ Отправки",
startingDaemon: "Запуск демона",
startingWallet: "Запуск кошелька",
switchToDateSelect: "Переключиться на выбор даты",
switchToHeightSelect: "Переключиться на выбор высоты блока",
transactionID: "ID транзакции",
transactionConfirmed: "подтверждено",
transactions: {
amount: "Сумма",
description: "Транзакция: {type}",
fee: "Комиссия",
paidBySender: "оплачивается отправителем",
received: "Получено",
sent: "Отправлено",
sentTo: "Транзакция отправлена: {type}",
timestamp: "Отметка времени",
types: {
all: "Все",
incoming: "Входящие",
outgoing: "Исходящие",
pending: "Ожидающие",
pendingIncoming: "Ожидающие входящие",
pendingOutgoing: "Ожидающие исходящие",
miner: "Майнинг",
serviceNode: "Сервисная Нода",
governance: "Управление",
stake: "Стейк",
failed: "Не удавшиеся"
}
},
unspentOutputs: "Непотраченные выходы",
userNotUsedAddress: "Вы не использовали этот адрес",
userUsedAddress: "Вы использовали этот адрес",
viewKey: "Ключ Просмотра",
viewOnlyMode: "Режим просмотра. Пожалуйста загрузите полный кошелек, чтобы отправлять монеты."
},
titles: {
addressBook: "Адресная книга",
addressDetails: "Данные адреса",
advanced: {
checkTransaction: "CHECK TRANSACTION",
prove: "PROVE"
},
changePassword: "Сменить пароль",
configure: "Настройки",
currentlyStakedNodes: "Currently staked nodes",
privateKeys: "Приватные ключи",
rescanWallet: "Пересканировать кошелек",
serviceNode: {
registration: "РЕГИСТРАЦИЯ",
staking: "СТЕЙК"
},
settings: {
title: "Настройки",
tabs: {
general: "Общие",
language: "Язык",
peers: "Пиры"
}
},
transactionDetails: "Данные транзакции",
transactions: "Транзакции",
wallet: {
createNew: "Создать новый кошелек",
createdOrRestored: "Кошелек создан/восстановлен",
importFromFile: "Импортировать кошелек из файла",
importFromLegacyGUI: "Импортировать кошелек из предыдущего GUI",
importFromOldGUI: "Импортировать кошелек из старого GUI",
restoreFromSeed: "Восстановить кошелек из seed-фразы",
restoreViewOnly: "Восстановить кошелек только-для-чтения"
},
welcome: "Добро пожаловать",
yourWallets: "Ваши Кошельки"
}
};

View File

@ -9,13 +9,10 @@
<script>
export default {
// name: 'LayoutName',
data () {
return {
}
data() {
return {};
}
}
};
</script>
<style>
</style>
<style></style>

View File

@ -1,22 +1,18 @@
<template>
<q-layout view="hHh Lpr lFf">
<q-layout view="hHh Lpr lFf">
<q-page-container>
<router-view />
<router-view />
</q-page-container>
</q-layout>
</q-layout>
</template>
<script>
export default {
// name: 'LayoutName',
data () {
return {
}
}
}
// name: 'LayoutName',
data() {
return {};
}
};
</script>
<style>
</style>
<style></style>

View File

@ -1,92 +1,82 @@
<template>
<q-layout view="hHh Lpr lFf">
<q-layout view="hHh Lpr lFf">
<q-layout-header class="shift-title">
<template v-if="show_menu">
<MainMenu :disable-switch-wallet="true" />
</template>
<template v-else>
<q-btn class="cancel" icon="reply" flat round dense @click="cancel()" />
</template>
<template v-if="show_menu">
<main-menu :disable-switch-wallet="true" />
</template>
<template v-else>
<q-btn class="cancel" icon="reply"
flat round dense
@click="cancel()" />
</template>
<q-toolbar-title v-if="page_title=='Loki'">
<div class="flex items-center justify-center" style="margin-top:7px">
<img src="statics/loki.svg" height="32">
</div>
</q-toolbar-title>
<q-toolbar-title v-else>
{{ page_title }}
</q-toolbar-title>
<q-toolbar-title v-if="page_title == 'Loki'">
<div class="flex items-center justify-center" style="margin-top:7px">
<img src="statics/loki.svg" height="32" />
</div>
</q-toolbar-title>
<q-toolbar-title v-else>
{{ page_title }}
</q-toolbar-title>
</q-layout-header>
<q-page-container>
<router-view ref="page" />
<router-view ref="page" />
</q-page-container>
<status-footer />
</q-layout>
<StatusFooter />
</q-layout>
</template>
<script>
import { mapState } from "vuex"
import SettingsModal from "components/settings"
import StatusFooter from "components/footer"
import MainMenu from "components/mainmenu"
import StatusFooter from "components/footer";
import MainMenu from "components/mainmenu";
export default {
data() {
return {
}
components: {
StatusFooter,
MainMenu
},
data() {
return {};
},
computed: {
show_menu() {
return this.$route.name === "wallet-select";
},
computed: {
show_menu () {
return this.$route.name === "wallet-select"
},
page_title () {
switch(this.$route.name) {
case "wallet-create":
return this.$t("titles.wallet.createNew")
case "wallet-restore":
return this.$t("titles.wallet.restoreFromSeed")
case "wallet-import":
return this.$t("titles.wallet.importFromFile")
case "wallet-import-view-only":
return this.$t("titles.wallet.restoreViewOnly")
case "wallet-import-legacy":
return this.$t("titles.wallet.importFromLegacyGUI")
case "wallet-import-old-gui":
return this.$t("titles.wallet.importFromOldGUI")
case "wallet-created":
return this.$t("titles.wallet.createdOrRestored")
page_title() {
switch (this.$route.name) {
case "wallet-create":
return this.$t("titles.wallet.createNew");
case "wallet-restore":
return this.$t("titles.wallet.restoreFromSeed");
case "wallet-import":
return this.$t("titles.wallet.importFromFile");
case "wallet-import-view-only":
return this.$t("titles.wallet.restoreViewOnly");
case "wallet-import-legacy":
return this.$t("titles.wallet.importFromLegacyGUI");
case "wallet-import-old-gui":
return this.$t("titles.wallet.importFromOldGUI");
case "wallet-created":
return this.$t("titles.wallet.createdOrRestored");
default:
case "wallet-select":
return "Loki"
}
}
},
methods: {
cancel() {
this.$router.replace({ path: "/wallet-select" });
this.$gateway.send("wallet", "close_wallet")
setTimeout(() => {
// short delay to prevent wallet data reaching the
// websocket moments after we close and reset data
this.$store.dispatch("gateway/resetWalletData")
}, 250);
}
},
components: {
StatusFooter,
MainMenu
default:
case "wallet-select":
return "Loki";
}
}
}
},
methods: {
cancel() {
this.$router.replace({ path: "/wallet-select" });
this.$gateway.send("wallet", "close_wallet");
setTimeout(() => {
// short delay to prevent wallet data reaching the
// websocket moments after we close and reset data
this.$store.dispatch("gateway/resetWalletData");
}, 250);
}
}
};
</script>
<style>
</style>
<style></style>

View File

@ -1,144 +1,106 @@
<template>
<q-layout view="hHh Lpr lFf">
<q-layout view="hHh Lpr lFf">
<q-layout-header class="shift-title">
<main-menu />
<q-toolbar-title>
<div class="flex items-center justify-center" style="margin-top:7px">
<img src="statics/loki.svg" height="32">
</div>
</q-toolbar-title>
<MainMenu />
<q-toolbar-title>
<div class="flex items-center justify-center" style="margin-top:7px">
<img src="statics/loki.svg" height="32" />
</div>
</q-toolbar-title>
</q-layout-header>
<q-page-container>
<!-- <AddressHeader :address="info.address" :title="info.name" /> -->
<WalletDetails />
<!-- <AddressHeader :address="info.address" :title="info.name" /> -->
<WalletDetails />
<div class="app-content">
<div class="navigation row items-end">
<router-link to="/wallet">
<q-btn
class="single-icon"
size="md"
icon="swap_horiz"
/>
</router-link>
<router-link to="/wallet/send">
<q-btn
class="large-btn"
:label="$t('buttons.send')"
size="md"
icon-right="arrow_right_alt"
align="left"
/>
</router-link>
<router-link to="/wallet/receive">
<q-btn
class="large-btn"
:label="$t('buttons.receive')"
size="md"
icon-right="save_alt"
align="left"
/>
</router-link>
<router-link to="/wallet/servicenode">
<q-btn
class="large-btn"
:label="$t('buttons.serviceNode')"
size="md"
icon-right="router"
align="left"
/>
</router-link>
<router-link to="/wallet/advanced">
<q-btn
class="large-btn"
:label="$t('buttons.advanced')"
size="md"
icon-right="tune"
align="left"
/>
</router-link>
<router-link to="/wallet/addressbook" class="address">
<q-btn
class="single-icon"
size="md"
icon="person"
/>
</router-link>
</div>
<div class="hr-separator" />
<keep-alive>
<router-view />
</keep-alive>
<div class="app-content">
<div class="navigation row items-end">
<router-link to="/wallet">
<q-btn class="single-icon" size="md" icon="swap_horiz" />
</router-link>
<router-link to="/wallet/send">
<q-btn class="large-btn" :label="$t('buttons.send')" size="md" icon-right="arrow_right_alt" align="left" />
</router-link>
<router-link to="/wallet/receive">
<q-btn class="large-btn" :label="$t('buttons.receive')" size="md" icon-right="save_alt" align="left" />
</router-link>
<router-link to="/wallet/servicenode">
<q-btn class="large-btn" :label="$t('buttons.serviceNode')" size="md" icon-right="router" align="left" />
</router-link>
<router-link to="/wallet/advanced">
<q-btn class="large-btn" :label="$t('buttons.advanced')" size="md" icon-right="tune" align="left" />
</router-link>
<router-link to="/wallet/addressbook" class="address">
<q-btn class="single-icon" size="md" icon="person" />
</router-link>
</div>
<div class="hr-separator" />
<keep-alive>
<router-view />
</keep-alive>
</div>
</q-page-container>
<status-footer />
</q-layout>
<StatusFooter />
</q-layout>
</template>
<script>
const { clipboard } = require("electron")
import { openURL } from "quasar"
import { mapState } from "vuex"
import WalletDetails from "components/wallet_details"
import FormatLoki from "components/format_loki"
import StatusFooter from "components/footer"
import MainMenu from "components/mainmenu"
import { openURL } from "quasar";
import { mapState } from "vuex";
import WalletDetails from "components/wallet_details";
import StatusFooter from "components/footer";
import MainMenu from "components/mainmenu";
export default {
name: "LayoutDefault",
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
info: state => state.gateway.wallet.info,
}),
data() {
return {
selectedTab: "tab-1",
}
},
methods: {
openURL,
},
components: {
StatusFooter,
MainMenu,
WalletDetails
}
}
name: "LayoutDefault",
components: {
StatusFooter,
MainMenu,
WalletDetails
},
data() {
return {
selectedTab: "tab-1"
};
},
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
info: state => state.gateway.wallet.info
}),
methods: {
openURL
}
};
</script>
<style lang="scss">
.navigation {
padding: 8px 12px;
> * {
margin: 2px 0;
margin-right: 12px;
}
> *:last-child {
margin-right: 0px;
}
.address {
margin-left: auto;
}
.single-icon {
width: 38px;
padding: 0;
}
.large-btn {
min-width: 160px;
.q-btn-inner > *:last-child {
margin-left: auto;
padding-left: 8px;
}
padding: 8px 12px;
> * {
margin: 2px 0;
margin-right: 12px;
}
> *:last-child {
margin-right: 0px;
}
.address {
margin-left: auto;
}
.single-icon {
width: 38px;
padding: 0;
}
.large-btn {
min-width: 160px;
.q-btn-inner > *:last-child {
margin-left: auto;
padding-left: 8px;
}
}
}
</style>

View File

@ -1,38 +1,42 @@
import { mapState } from "vuex"
import { mapState } from "vuex";
export default {
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme
}),
methods: {
hasPassword () {
// Validate the address
return new Promise((resolve) => {
this.$gateway.once("has_password", (data) => {
resolve(!!data)
})
this.$gateway.send("wallet", "has_password")
})
},
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme
}),
methods: {
hasPassword() {
// Validate the address
return new Promise(resolve => {
this.$gateway.once("has_password", data => {
resolve(!!data);
});
this.$gateway.send("wallet", "has_password");
});
},
showPasswordConfirmation (options) {
const { noPasswordMessage, ...other } = options
showPasswordConfirmation(options) {
const { noPasswordMessage, ...other } = options;
return this.hasPassword().then(hasPassword => {
return this.$q.dialog({
cancel: {
flat: true,
label: this.$t("dialog.buttons.cancel"),
color: this.theme === "dark" ? "white" : "dark"
},
...other,
message: hasPassword ? this.$t("dialog.password.message") : noPasswordMessage,
prompt: hasPassword ? {
model: "",
type: "password"
} : null
})
}).then(password => password || "")
}
return this.hasPassword()
.then(hasPassword => {
return this.$q.dialog({
cancel: {
flat: true,
label: this.$t("dialog.buttons.cancel"),
color: this.theme === "dark" ? "white" : "dark"
},
...other,
message: hasPassword ? this.$t("dialog.password.message") : noPasswordMessage,
prompt: hasPassword
? {
model: "",
type: "password"
}
: null
});
})
.then(password => password || "");
}
}
}
};

View File

@ -1,5 +1,5 @@
<template>
<div class="fixed-center text-center">
<div class="fixed-center text-center">
internal error
</div>
</div>
</template>

View File

@ -1,168 +1,261 @@
<template>
<q-page>
<q-page>
<div class="init-screen-page text-center">
<div class="absolute-center">
<img src="statics/loki.svg" width="400" class="q-mb-md">
<div class="startup-icons q-mt-xl q-mb-lg">
<div ref="backend">
<svg width="64" viewBox="0 0 17 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="si-glyph si-glyph-link-3"><g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><path d="M16.969,2 C16.969,0.896 16.075,0 14.973,0 C13.871,0 13.014,0.896 13.014,2 C13.014,2.723 13.422,3.332 14,3.683 L14,7.021 L10.688,7.021 C10.345,6.42 9.722,6 8.982,6 C8.241,6 7.618,6.42 7.274,7.021 L3,7.021 C2.447,7.021 2,7.468 2,8.021 L2,12.29 C1.412,12.643 0.994,13.271 0.994,14.001 C0.994,15.105 1.889,16.001 2.99,16.001 C4.093,16.001 4.988,15.105 4.988,14.001 C4.988,13.282 4.576,12.675 4,12.323 L4,8.938 L7.252,8.938 C7.59,9.562 8.225,10 8.982,10 C9.739,10 10.373,9.562 10.711,8.938 L15,8.938 C15.553,8.938 16,8.491 16,7.938 L16,3.684 C16.574,3.333 16.969,2.723 16.969,2 L16.969,2 Z" fill="#434343" class="si-glyph-fill"></path></g></svg>
</div>
<div ref="settings">
<svg width="64" viewBox="0 0 17 17" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="si-glyph si-glyph-gear-1"><g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><g transform="translate(1.000000, 1.000000)" fill="#434343"><path d="M7.887,9.025 C7.799,8.449 7.569,7.92 7.229,7.475 L7.995,6.71 L7.307,6.023 L6.536,6.794 C6.093,6.467 5.566,6.245 4.994,6.161 L4.994,5.066 L4.021,5.066 L4.021,6.155 C3.444,6.232 2.913,6.452 2.461,6.777 L1.709,6.024 L1.021,6.712 L1.761,7.452 C1.411,7.901 1.175,8.437 1.087,9.024 L0.062,9.024 L0.062,9.025 L0.062,9.998 L1.08,9.998 C1.162,10.589 1.396,11.132 1.744,11.587 L1.02,12.31 L1.708,12.997 L2.437,12.268 C2.892,12.604 3.432,12.83 4.02,12.91 L4.02,13.958 L4.993,13.958 L4.993,12.904 C5.576,12.818 6.11,12.589 6.56,12.252 L7.306,12.999 L7.994,12.311 L7.248,11.564 C7.586,11.115 7.812,10.581 7.893,10 L8.952,10 L8.952,9.998 L8.952,9.026 L7.887,9.026 L7.887,9.025 Z M4.496,11.295 C3.512,11.295 2.715,10.497 2.715,9.512 C2.715,8.528 3.512,7.73 4.496,7.73 C5.481,7.73 6.28,8.528 6.28,9.512 C6.28,10.497 5.481,11.295 4.496,11.295 L4.496,11.295 Z" class="si-glyph-fill"></path><path d="M13.031,3.37 L14.121,3.089 L13.869,2.11 L12.778,2.392 C12.66,2.152 12.513,1.922 12.317,1.72 C12.125,1.524 11.902,1.376 11.67,1.256 L11.971,0.177 L10.998,-0.094 L10.699,0.978 C10.158,0.935 9.608,1.056 9.133,1.36 L8.373,0.584 L7.652,1.291 L8.408,2.061 C8.082,2.531 7.939,3.085 7.967,3.636 L6.927,3.904 L7.179,4.881 L8.217,4.613 C8.334,4.856 8.483,5.088 8.682,5.291 C8.885,5.499 9.121,5.653 9.368,5.776 L9.079,6.815 L10.05,7.086 L10.343,6.038 C10.885,6.071 11.435,5.938 11.906,5.623 L12.677,6.409 L13.397,5.702 L12.621,4.911 C12.928,4.446 13.06,3.905 13.031,3.37 L13.031,3.37 Z M10.514,4.987 C9.691,4.987 9.023,4.318 9.023,3.494 C9.023,2.672 9.691,2.005 10.514,2.005 C11.336,2.005 12.004,2.672 12.004,3.494 C12.004,4.318 11.336,4.987 10.514,4.987 L10.514,4.987 Z" class="si-glyph-fill"></path></g></g></svg>
</div>
<div ref="daemon">
<svg width="64" viewBox="0 0 16 17" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="si-glyph si-glyph-network-2"><defs></defs><g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><g transform="translate(1.000000, 1.000000)" fill="#434343"><path d="M7.494,14.958 C3.361,14.958 0,11.622 0,7.52 C0,3.418 3.361,0.082 7.494,0.082 C11.627,0.082 14.989,3.418 14.989,7.52 C14.989,11.622 11.627,14.958 7.494,14.958 L7.494,14.958 Z M7.51,0.938 C3.887,0.938 0.938,3.886 0.938,7.51 C0.938,11.135 3.887,14.083 7.51,14.083 C11.135,14.083 14.083,11.135 14.083,7.51 C14.083,3.886 11.135,0.938 7.51,0.938 L7.51,0.938 Z" class="si-glyph-fill"></path><rect x="7" y="1" width="0.922" height="14.084" class="si-glyph-fill"></rect><rect x="0" y="7" width="13.96" height="0.922" class="si-glyph-fill"></rect><rect x="1" y="4" width="12.406" height="0.906" class="si-glyph-fill"></rect><rect x="1" y="10" width="12.406" height="0.922" class="si-glyph-fill"></rect><path d="M7.317,14.854 C4.72,13.581 3.043,10.662 3.043,7.417 C3.043,4.247 4.666,1.355 7.181,0.05 L7.642,0.937 C5.455,2.074 4.043,4.617 4.043,7.417 C4.043,10.282 5.502,12.849 7.757,13.955 L7.317,14.854 L7.317,14.854 Z" class="si-glyph-fill"></path><path d="M7.74,14.789 L7.271,13.906 C9.41,12.772 10.792,10.225 10.792,7.417 C10.792,4.642 9.433,2.107 7.332,0.96 L7.811,0.083 C10.229,1.401 11.792,4.28 11.792,7.417 C11.793,10.592 10.201,13.485 7.74,14.789 L7.74,14.789 Z" class="si-glyph-fill"></path></g></g></svg>
</div>
<div ref="wallet">
<svg width="64" viewBox="0 0 17 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="si-glyph si-glyph-wallet"><defs class="si-glyph-fill"></defs><g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><g transform="translate(1.000000, 0.000000)" fill="#434343"><path d="M7.988,10.635 L7.988,8.327 C7.988,7.578 8.561,6.969 9.267,6.969 L13.964,6.969 L13.964,5.531 C13.964,4.849 13.56,4.279 13.007,4.093 L13.007,4.094 L11.356,4.08 L11.336,4.022 L3.925,4.022 L3.784,4.07 L1.17,4.068 L1.165,4.047 C0.529,4.167 0.017,4.743 0.017,5.484 L0.017,13.437 C0.017,14.269 0.665,14.992 1.408,14.992 L12.622,14.992 C13.365,14.992 13.965,14.316 13.965,13.484 L13.965,12.031 L9.268,12.031 C8.562,12.031 7.988,11.384 7.988,10.635 L7.988,10.635 Z" class="si-glyph-fill"></path><path d="M14.996,8.061 L14.947,8.061 L9.989,8.061 C9.46,8.061 9.031,8.529 9.031,9.106 L9.031,9.922 C9.031,10.498 9.46,10.966 9.989,10.966 L14.947,10.966 L14.996,10.966 C15.525,10.966 15.955,10.498 15.955,9.922 L15.955,9.106 C15.955,8.528 15.525,8.061 14.996,8.061 L14.996,8.061 Z M12.031,10.016 L9.969,10.016 L9.969,9 L12.031,9 L12.031,10.016 L12.031,10.016 Z" class="si-glyph-fill"></path><path d="M3.926,4.022 L10.557,1.753 L11.337,4.022 L12.622,4.022 C12.757,4.022 12.885,4.051 13.008,4.092 L11.619,0.051 L1.049,3.572 L1.166,4.048 C1.245,4.033 1.326,4.023 1.408,4.023 L3.926,4.023 L3.926,4.022 Z" class="si-glyph-fill"></path></g></g></svg>
</div>
</div>
<div class="message">
{{ message }}
</div>
<div class="q-mt-xs" v-if="daemonStatus">
{{ $t('strings.syncingDaemon') }}: {{ daemonStatus }}
</div>
<div class="absolute-center">
<img src="statics/loki.svg" width="400" class="q-mb-md" />
<div class="startup-icons q-mt-xl q-mb-lg">
<div ref="backend">
<svg
width="64"
viewBox="0 0 17 16"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
class="si-glyph si-glyph-link-3"
>
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path
d="M16.969,2 C16.969,0.896 16.075,0 14.973,0 C13.871,0 13.014,0.896 13.014,2 C13.014,2.723 13.422,3.332 14,3.683 L14,7.021 L10.688,7.021 C10.345,6.42 9.722,6 8.982,6 C8.241,6 7.618,6.42 7.274,7.021 L3,7.021 C2.447,7.021 2,7.468 2,8.021 L2,12.29 C1.412,12.643 0.994,13.271 0.994,14.001 C0.994,15.105 1.889,16.001 2.99,16.001 C4.093,16.001 4.988,15.105 4.988,14.001 C4.988,13.282 4.576,12.675 4,12.323 L4,8.938 L7.252,8.938 C7.59,9.562 8.225,10 8.982,10 C9.739,10 10.373,9.562 10.711,8.938 L15,8.938 C15.553,8.938 16,8.491 16,7.938 L16,3.684 C16.574,3.333 16.969,2.723 16.969,2 L16.969,2 Z"
fill="#434343"
class="si-glyph-fill"
></path>
</g>
</svg>
</div>
<div ref="settings">
<svg
width="64"
viewBox="0 0 17 17"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
class="si-glyph si-glyph-gear-1"
>
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g transform="translate(1.000000, 1.000000)" fill="#434343">
<path
d="M7.887,9.025 C7.799,8.449 7.569,7.92 7.229,7.475 L7.995,6.71 L7.307,6.023 L6.536,6.794 C6.093,6.467 5.566,6.245 4.994,6.161 L4.994,5.066 L4.021,5.066 L4.021,6.155 C3.444,6.232 2.913,6.452 2.461,6.777 L1.709,6.024 L1.021,6.712 L1.761,7.452 C1.411,7.901 1.175,8.437 1.087,9.024 L0.062,9.024 L0.062,9.025 L0.062,9.998 L1.08,9.998 C1.162,10.589 1.396,11.132 1.744,11.587 L1.02,12.31 L1.708,12.997 L2.437,12.268 C2.892,12.604 3.432,12.83 4.02,12.91 L4.02,13.958 L4.993,13.958 L4.993,12.904 C5.576,12.818 6.11,12.589 6.56,12.252 L7.306,12.999 L7.994,12.311 L7.248,11.564 C7.586,11.115 7.812,10.581 7.893,10 L8.952,10 L8.952,9.998 L8.952,9.026 L7.887,9.026 L7.887,9.025 Z M4.496,11.295 C3.512,11.295 2.715,10.497 2.715,9.512 C2.715,8.528 3.512,7.73 4.496,7.73 C5.481,7.73 6.28,8.528 6.28,9.512 C6.28,10.497 5.481,11.295 4.496,11.295 L4.496,11.295 Z"
class="si-glyph-fill"
></path>
<path
d="M13.031,3.37 L14.121,3.089 L13.869,2.11 L12.778,2.392 C12.66,2.152 12.513,1.922 12.317,1.72 C12.125,1.524 11.902,1.376 11.67,1.256 L11.971,0.177 L10.998,-0.094 L10.699,0.978 C10.158,0.935 9.608,1.056 9.133,1.36 L8.373,0.584 L7.652,1.291 L8.408,2.061 C8.082,2.531 7.939,3.085 7.967,3.636 L6.927,3.904 L7.179,4.881 L8.217,4.613 C8.334,4.856 8.483,5.088 8.682,5.291 C8.885,5.499 9.121,5.653 9.368,5.776 L9.079,6.815 L10.05,7.086 L10.343,6.038 C10.885,6.071 11.435,5.938 11.906,5.623 L12.677,6.409 L13.397,5.702 L12.621,4.911 C12.928,4.446 13.06,3.905 13.031,3.37 L13.031,3.37 Z M10.514,4.987 C9.691,4.987 9.023,4.318 9.023,3.494 C9.023,2.672 9.691,2.005 10.514,2.005 C11.336,2.005 12.004,2.672 12.004,3.494 C12.004,4.318 11.336,4.987 10.514,4.987 L10.514,4.987 Z"
class="si-glyph-fill"
></path>
</g>
</g>
</svg>
</div>
<div ref="daemon">
<svg
width="64"
viewBox="0 0 16 17"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
class="si-glyph si-glyph-network-2"
>
<defs></defs>
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g transform="translate(1.000000, 1.000000)" fill="#434343">
<path
d="M7.494,14.958 C3.361,14.958 0,11.622 0,7.52 C0,3.418 3.361,0.082 7.494,0.082 C11.627,0.082 14.989,3.418 14.989,7.52 C14.989,11.622 11.627,14.958 7.494,14.958 L7.494,14.958 Z M7.51,0.938 C3.887,0.938 0.938,3.886 0.938,7.51 C0.938,11.135 3.887,14.083 7.51,14.083 C11.135,14.083 14.083,11.135 14.083,7.51 C14.083,3.886 11.135,0.938 7.51,0.938 L7.51,0.938 Z"
class="si-glyph-fill"
></path>
<rect x="7" y="1" width="0.922" height="14.084" class="si-glyph-fill"></rect>
<rect x="0" y="7" width="13.96" height="0.922" class="si-glyph-fill"></rect>
<rect x="1" y="4" width="12.406" height="0.906" class="si-glyph-fill"></rect>
<rect x="1" y="10" width="12.406" height="0.922" class="si-glyph-fill"></rect>
<path
d="M7.317,14.854 C4.72,13.581 3.043,10.662 3.043,7.417 C3.043,4.247 4.666,1.355 7.181,0.05 L7.642,0.937 C5.455,2.074 4.043,4.617 4.043,7.417 C4.043,10.282 5.502,12.849 7.757,13.955 L7.317,14.854 L7.317,14.854 Z"
class="si-glyph-fill"
></path>
<path
d="M7.74,14.789 L7.271,13.906 C9.41,12.772 10.792,10.225 10.792,7.417 C10.792,4.642 9.433,2.107 7.332,0.96 L7.811,0.083 C10.229,1.401 11.792,4.28 11.792,7.417 C11.793,10.592 10.201,13.485 7.74,14.789 L7.74,14.789 Z"
class="si-glyph-fill"
></path>
</g>
</g>
</svg>
</div>
<div ref="wallet">
<svg
width="64"
viewBox="0 0 17 16"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
class="si-glyph si-glyph-wallet"
>
<defs class="si-glyph-fill"></defs>
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g transform="translate(1.000000, 0.000000)" fill="#434343">
<path
d="M7.988,10.635 L7.988,8.327 C7.988,7.578 8.561,6.969 9.267,6.969 L13.964,6.969 L13.964,5.531 C13.964,4.849 13.56,4.279 13.007,4.093 L13.007,4.094 L11.356,4.08 L11.336,4.022 L3.925,4.022 L3.784,4.07 L1.17,4.068 L1.165,4.047 C0.529,4.167 0.017,4.743 0.017,5.484 L0.017,13.437 C0.017,14.269 0.665,14.992 1.408,14.992 L12.622,14.992 C13.365,14.992 13.965,14.316 13.965,13.484 L13.965,12.031 L9.268,12.031 C8.562,12.031 7.988,11.384 7.988,10.635 L7.988,10.635 Z"
class="si-glyph-fill"
></path>
<path
d="M14.996,8.061 L14.947,8.061 L9.989,8.061 C9.46,8.061 9.031,8.529 9.031,9.106 L9.031,9.922 C9.031,10.498 9.46,10.966 9.989,10.966 L14.947,10.966 L14.996,10.966 C15.525,10.966 15.955,10.498 15.955,9.922 L15.955,9.106 C15.955,8.528 15.525,8.061 14.996,8.061 L14.996,8.061 Z M12.031,10.016 L9.969,10.016 L9.969,9 L12.031,9 L12.031,10.016 L12.031,10.016 Z"
class="si-glyph-fill"
></path>
<path
d="M3.926,4.022 L10.557,1.753 L11.337,4.022 L12.622,4.022 C12.757,4.022 12.885,4.051 13.008,4.092 L11.619,0.051 L1.049,3.572 L1.166,4.048 C1.245,4.033 1.326,4.023 1.408,4.023 L3.926,4.023 L3.926,4.022 Z"
class="si-glyph-fill"
></path>
</g>
</g>
</svg>
</div>
</div>
<div class="absolute-bottom">
{{ version }}
<div class="message">
{{ message }}
</div>
<div v-if="daemonStatus" class="q-mt-xs">{{ $t("strings.syncingDaemon") }}: {{ daemonStatus }}</div>
</div>
<div class="absolute-bottom">
{{ version }}
</div>
</div>
</q-page>
</q-page>
</template>
<script>
import { mapState } from "vuex"
import { mapState } from "vuex";
export default {
data() {
return {
message: "",
version: ""
}
data() {
return {
message: "",
version: ""
};
},
computed: mapState({
status: state => state.gateway.app.status,
config: state => state.gateway.app.config,
isLocalDaemon() {
if (!this.config || !this.config.app.net_type) return false;
return this.config.daemons[this.config.app.net_type].type === "local";
},
computed: mapState({
status: state => state.gateway.app.status,
config: state => state.gateway.app.config,
isLocalDaemon (state) {
if (!this.config || !this.config.app.net_type) return false
return this.config.daemons[this.config.app.net_type].type === "local"
},
daemon: state => state.gateway.daemon,
daemonStatus (state) {
// Check to see if config is loaded
if (this.status.code < 3 || !this.isLocalDaemon) return null
daemon: state => state.gateway.daemon,
daemonStatus() {
// Check to see if config is loaded
if (this.status.code < 3 || !this.isLocalDaemon) return null;
const currentHeight = this.daemon.info.height_without_bootstrap
const targetHeight = Math.max(this.daemon.info.height, this.daemon.info.target_height)
const percentage = (100 * currentHeight / targetHeight).toFixed(1)
const currentHeight = this.daemon.info.height_without_bootstrap;
const targetHeight = Math.max(this.daemon.info.height, this.daemon.info.target_height);
const percentage = ((100 * currentHeight) / targetHeight).toFixed(1);
if (targetHeight === 0 || currentHeight >= targetHeight) return null
if (targetHeight === 0 || currentHeight >= targetHeight) return null;
return `${currentHeight}/${targetHeight} (${percentage}%)`
},
}),
methods: {
updateStatus() {
switch(this.status.code) {
case -1: // config not found, go to welcome screen
this.$router.replace({ path: "welcome" });
break;
case 0: // start-up complete, go to wallet-select
this.$router.replace({ path: "wallet-select" });
break;
case 1:
this.message = this.$t("strings.connectingToBackend")
this.$refs.backend.className = "pulse"
this.$refs.settings.className = "grey"
this.$refs.daemon.className = "grey"
this.$refs.wallet.className = "grey"
break;
case 2:
this.message = this.$t("strings.loadingSettings")
this.$refs.backend.className = "solid"
this.$refs.settings.className = "pulse"
this.$refs.daemon.className = "grey"
this.$refs.wallet.className = "grey"
break;
case 3:
this.message = this.$t("strings.startingDaemon")
this.$refs.backend.className = "solid"
this.$refs.settings.className = "solid"
this.$refs.daemon.className = "pulse"
this.$refs.wallet.className = "grey"
break;
case 4:
this.version = this.status.message
break;
case 5:
this.$q.notify({
type: "warning",
timeout: 2000,
message: "Warning: " + this.$t("notification.warnings.usingRemoteNode")
})
break;
case 6:
this.message = this.$t("strings.startingWallet")
this.$refs.backend.className = "solid"
this.$refs.settings.className = "solid"
this.$refs.daemon.className = "solid"
this.$refs.wallet.className = "pulse"
break;
case 7:
this.message = this.$t("strings.readingWalletList")
this.$refs.backend.className = "solid"
this.$refs.settings.className = "solid"
this.$refs.daemon.className = "solid"
this.$refs.wallet.className = "solid"
break;
}
}
},
mounted () {
this.updateStatus()
},
watch: {
status: {
handler(){
this.updateStatus()
},
deep: true
}
return `${currentHeight}/${targetHeight} (${percentage}%)`;
}
}
}),
watch: {
status: {
handler() {
this.updateStatus();
},
deep: true
}
},
mounted() {
this.updateStatus();
},
methods: {
updateStatus() {
switch (this.status.code) {
case -1: // config not found, go to welcome screen
this.$router.replace({ path: "welcome" });
break;
case 0: // start-up complete, go to wallet-select
this.$router.replace({ path: "wallet-select" });
break;
case 1:
this.message = this.$t("strings.connectingToBackend");
this.$refs.backend.className = "pulse";
this.$refs.settings.className = "grey";
this.$refs.daemon.className = "grey";
this.$refs.wallet.className = "grey";
break;
case 2:
this.message = this.$t("strings.loadingSettings");
this.$refs.backend.className = "solid";
this.$refs.settings.className = "pulse";
this.$refs.daemon.className = "grey";
this.$refs.wallet.className = "grey";
break;
case 3:
this.message = this.$t("strings.startingDaemon");
this.$refs.backend.className = "solid";
this.$refs.settings.className = "solid";
this.$refs.daemon.className = "pulse";
this.$refs.wallet.className = "grey";
break;
case 4:
this.version = this.status.message;
break;
case 5:
this.$q.notify({
type: "warning",
timeout: 2000,
message: "Warning: " + this.$t("notification.warnings.usingRemoteNode")
});
break;
case 6:
this.message = this.$t("strings.startingWallet");
this.$refs.backend.className = "solid";
this.$refs.settings.className = "solid";
this.$refs.daemon.className = "solid";
this.$refs.wallet.className = "pulse";
break;
case 7:
this.message = this.$t("strings.readingWalletList");
this.$refs.backend.className = "solid";
this.$refs.settings.className = "solid";
this.$refs.daemon.className = "solid";
this.$refs.wallet.className = "solid";
break;
}
}
}
};
</script>
<style lang="scss">
.init-screen-page {
height: 100vh;
position: relative;
height: 100vh;
position: relative;
}
.startup-icons {
&>div {
display: inline-block;
margin: 0 15px;
color: #444;
g,path {
fill: #444;
}
& > div {
display: inline-block;
margin: 0 15px;
color: #444;
g,
path {
fill: #444;
}
.pulse {
color:#cecece;
opacity: 0.6;
animation: fade 2s infinite;
g,path {
fill:#cecece;
}
}
.pulse {
color: #cecece;
opacity: 0.6;
animation: fade 2s infinite;
g,
path {
fill: #cecece;
}
}
}
@keyframes fade {
0%,100% { opacity: 0.3 }
50% { opacity: 0.6 }
0%,
100% {
opacity: 0.3;
}
50% {
opacity: 0.6;
}
}
</style>

View File

@ -1,29 +1,26 @@
<template>
<q-page>
<q-page>
<div class="init-screen-page text-center">
<div class="absolute-center">
<img src="statics/loki.svg" width="400" class="q-mb-md">
<div class="absolute-center">
<img src="statics/loki.svg" width="400" class="q-mb-md" />
<div class="q-mt-xl q-mb-lg">
<q-spinner color="primary" :size="30" />
</div>
<div class="message">
{{ $t("strings.closing") }}...
</div>
<div class="q-mt-xl q-mb-lg">
<q-spinner color="primary" :size="30" />
</div>
<div class="message">{{ $t("strings.closing") }}...</div>
</div>
</div>
</q-page>
</q-page>
</template>
<script>
export default {
}
export default {};
</script>
<style lang="scss">
.init-screen-page {
height: 100vh;
position: relative;
height: 100vh;
position: relative;
}
</style>

View File

@ -1,156 +1,134 @@
<template>
<q-page class="welcome">
<q-page class="welcome">
<q-stepper ref="stepper" class="no-shadow" :color="theme == 'dark' ? 'light' : 'dark'" dark @step="onStep">
<q-step default :title="$t('titles.welcome')" class="first-step">
<div class="welcome-container">
<img src="statics/loki.svg" height="100" class="q-mb-md" />
<div>Wallet Version: v{{ version }}</div>
<div>Deamon Version: v{{ daemonVersion }}</div>
<q-stepper class="no-shadow" ref="stepper" :color="theme == 'dark' ? 'light' : 'dark'" dark @step="onStep">
<LanguageSelect class="q-mt-lg" @select="onLanguageSelected" />
</div>
</q-step>
<q-step default :title="$t('titles.welcome')" class="first-step">
<div class="welcome-container">
<img src="statics/loki.svg" height="100" class="q-mb-md">
<div>Wallet Version: v{{version}}</div>
<div>Deamon Version: v{{daemonVersion}}</div>
<language-select
class="q-mt-lg"
v-on:select="onLanguageSelected"
/>
</div>
</q-step>
<q-step :title="$t('titles.configure')">
<SettingsGeneral randomise_remote ref="settingsGeneral" />
</q-step>
<q-step :title="$t('titles.configure')">
<SettingsGeneral ref="settingsGeneral" randomise_remote />
</q-step>
</q-stepper>
<q-layout-footer v-if="!is_first_page" class="no-shadow q-pa-sm">
<div class="row justify-end">
<div>
<q-btn
flat
@click="clickPrev()"
:label="$t('buttons.back')"
/>
</div>
<div>
<q-btn
class="q-ml-sm"
color="primary"
@click="clickNext()"
:label="$t('buttons.next')"
/>
</div>
<q-layout-footer v-if="!is_first_page" class="no-shadow q-pa-sm">
<div class="row justify-end">
<div>
<q-btn flat :label="$t('buttons.back')" @click="clickPrev()" />
</div>
<div>
<q-btn class="q-ml-sm" color="primary" :label="$t('buttons.next')" @click="clickNext()" />
</div>
</div>
</q-layout-footer>
</q-page>
</q-page>
</template>
<script>
import { version } from "../../../package.json"
import { mapState } from "vuex"
import LanguageSelect from "components/language_select"
import SettingsGeneral from "components/settings_general"
import { version } from "../../../package.json";
import { mapState } from "vuex";
import LanguageSelect from "components/language_select";
import SettingsGeneral from "components/settings_general";
export default {
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
pending_config: state => state.gateway.app.pending_config,
config_daemon (state) {
return this.pending_config.daemons[this.pending_config.app.net_type]
},
daemon: state => state.gateway.daemon,
daemonVersion (state) {
return this.daemon.info.version || 'N/A'
}
}),
data() {
return {
is_first_page: true,
version: "",
}
components: {
LanguageSelect,
SettingsGeneral
},
data() {
return {
is_first_page: true,
version: ""
};
},
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
pending_config: state => state.gateway.app.pending_config,
config_daemon() {
return this.pending_config.daemons[this.pending_config.app.net_type];
},
mounted () {
this.version = version
// set add status back to 2
this.$store.commit("gateway/set_app_data", {
status: {
code: 2 // Loading config
}
});
},
methods: {
onStep () {
this.is_first_page = this.$refs.stepper.steps[0].active
},
clickNext () {
if(this.$refs.stepper.steps[this.$refs.stepper.length-1].active) {
this.$gateway.send("core", "save_config_init", this.pending_config);
this.$router.replace({ path: "/" });
} else {
this.$refs.stepper.next();
}
},
clickPrev () {
this.$refs.stepper.previous();
},
onLanguageSelected (lang) {
this.clickNext()
}
},
components: {
LanguageSelect,
SettingsGeneral
daemon: state => state.gateway.daemon,
daemonVersion() {
return this.daemon.info.version || "N/A";
}
}
}),
mounted() {
this.version = version;
// set add status back to 2
this.$store.commit("gateway/set_app_data", {
status: {
code: 2 // Loading config
}
});
},
methods: {
onStep() {
this.is_first_page = this.$refs.stepper.steps[0].active;
},
clickNext() {
if (this.$refs.stepper.steps[this.$refs.stepper.length - 1].active) {
this.$gateway.send("core", "save_config_init", this.pending_config);
this.$router.replace({ path: "/" });
} else {
this.$refs.stepper.next();
}
},
clickPrev() {
this.$refs.stepper.previous();
},
onLanguageSelected() {
this.clickNext();
}
}
};
</script>
<style lang="scss">
.welcome {
.welcome-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
}
.welcome-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
}
.first-step .q-stepper-step-inner {
min-height: 250px;
height: calc(100vh - 102px);
}
.first-step .q-stepper-step-inner {
min-height: 250px;
height: calc(100vh - 102px);
}
}
.language-item {
padding: 10px 30px 10px 20px;
border: 1px solid #ccc;
cursor: pointer;
padding: 10px 30px 10px 20px;
border: 1px solid #ccc;
cursor: pointer;
.language-item-circle {
background: #cc90e2;
width: 50px;
height: 50px;
border-radius: 25px;
display: inline-block;
line-height: 50px;
text-align:center;
color: white;
margin-right: 10px;
}
.language-item-circle {
background: #cc90e2;
width: 50px;
height: 50px;
border-radius: 25px;
display: inline-block;
line-height: 50px;
text-align: center;
color: white;
margin-right: 10px;
}
}
.q-stepper-header {
min-height: 50px;
.q-stepper-tab {
padding-top: 0;
padding-bottom: 0;
}
min-height: 50px;
.q-stepper-tab {
padding-top: 0;
padding-bottom: 0;
}
}
</style>

View File

@ -1,183 +1,177 @@
<template>
<q-page class="create-wallet">
<q-page class="create-wallet">
<div class="fields q-mx-md q-mt-md">
<LokiField :label="$t('fieldLabels.walletName')" :error="$v.wallet.name.$error">
<q-input
v-model="wallet.name"
@keyup.enter="create"
@blur="$v.wallet.name.$touch"
:dark="theme=='dark'"
:placeholder="$t('placeholders.walletName')"
hide-underline
/>
</LokiField>
<LokiField :label="$t('fieldLabels.walletName')" :error="$v.wallet.name.$error">
<q-input
v-model="wallet.name"
:dark="theme == 'dark'"
:placeholder="$t('placeholders.walletName')"
hide-underline
@keyup.enter="create"
@blur="$v.wallet.name.$touch"
/>
</LokiField>
<LokiField :label="$t('fieldLabels.seedLanguage')">
<q-select
v-model="wallet.language"
:options="languageOptions"
:dark="theme=='dark'"
hide-underline
/>
</LokiField>
<LokiField :label="$t('fieldLabels.seedLanguage')">
<q-select v-model="wallet.language" :options="languageOptions" :dark="theme == 'dark'" hide-underline />
</LokiField>
<LokiField :label="$t('fieldLabels.password')" optional>
<q-input
v-model="wallet.password"
@keyup.enter="create"
type="password"
:dark="theme=='dark'"
:placeholder="$t('placeholders.walletPassword')"
hide-underline
/>
</LokiField>
<LokiField :label="$t('fieldLabels.password')" optional>
<q-input
v-model="wallet.password"
type="password"
:dark="theme == 'dark'"
:placeholder="$t('placeholders.walletPassword')"
hide-underline
@keyup.enter="create"
/>
</LokiField>
<LokiField :label="$t('fieldLabels.confirmPassword')">
<q-input
v-model="wallet.password_confirm"
@keyup.enter="create"
type="password"
:dark="theme=='dark'"
hide-underline
/>
</LokiField>
<q-field>
<q-btn color="primary" @click="create" :label="$t('buttons.createWallet')" />
</q-field>
<LokiField :label="$t('fieldLabels.confirmPassword')">
<q-input
v-model="wallet.password_confirm"
type="password"
:dark="theme == 'dark'"
hide-underline
@keyup.enter="create"
/>
</LokiField>
<q-field>
<q-btn color="primary" :label="$t('buttons.createWallet')" @click="create" />
</q-field>
</div>
</q-page>
</q-page>
</template>
<script>
import { required } from "vuelidate/lib/validators"
import { mapState } from "vuex"
import LokiField from "components/loki_field"
import { required } from "vuelidate/lib/validators";
import { mapState } from "vuex";
import LokiField from "components/loki_field";
export default {
data () {
return {
wallet: {
name: "",
language: "English",
password: "",
password_confirm: ""
},
components: {
LokiField
},
data() {
return {
wallet: {
name: "",
language: "English",
password: "",
password_confirm: ""
},
languageOptions: [
{label: "English", value: "English"},
{label: "Deutsch", value: "Deutsch"},
{label: "Español", value: "Español"},
{label: "Français", value: "Français"},
{label: "Italiano", value: "Italiano"},
{label: "Nederlands", value: "Nederlands"},
{label: "Português", value: "Português"},
{label: "Русский", value: "Русский"},
{label: "日本語", value: "日本語"},
{label: "简体中文 (中国)", value: "简体中文 (中国)"},
{label: "Esperanto", value: "Esperanto"},
{label: "Lojban", value: "Lojban"}
]
languageOptions: [
{ label: "English", value: "English" },
{ label: "Deutsch", value: "Deutsch" },
{ label: "Español", value: "Español" },
{ label: "Français", value: "Français" },
{ label: "Italiano", value: "Italiano" },
{ label: "Nederlands", value: "Nederlands" },
{ label: "Português", value: "Português" },
{ label: "Русский", value: "Русский" },
{ label: "日本語", value: "日本語" },
{ label: "简体中文 (中国)", value: "简体中文 (中国)" },
{ label: "Esperanto", value: "Esperanto" },
{ label: "Lojban", value: "Lojban" }
]
};
},
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
status: state => state.gateway.wallet.status
}),
watch: {
status: {
handler(val, old) {
if (val.code == old.code) return;
switch (this.status.code) {
case 1:
break;
case 0:
this.$q.loading.hide();
this.$router.replace({
path: "/wallet-select/created"
});
break;
default:
this.$q.loading.hide();
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.status.message
});
break;
}
},
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
status: state => state.gateway.wallet.status,
}),
watch: {
status: {
handler(val, old){
if(val.code == old.code) return
switch(this.status.code) {
case 1:
break;
case 0:
this.$q.loading.hide()
this.$router.replace({ path: "/wallet-select/created" });
break;
default:
this.$q.loading.hide()
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.status.message
})
break;
}
},
deep: true
}
},
validations: {
wallet: {
name: { required }
}
},
methods: {
create() {
this.$v.wallet.$touch()
if (this.$v.wallet.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.enterWalletName")
})
return
}
if(this.wallet.password != this.wallet.password_confirm) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.passwordNoMatch")
})
return
}
// Warn user if no password is set
let passwordPromise = Promise.resolve();
if (!this.wallet.password) {
passwordPromise = this.$q.dialog({
title: this.$t("dialog.noPassword.title"),
message: this.$t("dialog.noPassword.message"),
ok: {
label: this.$t("dialog.noPassword.ok"),
},
cancel: {
flat: true,
label: this.$t("dialog.buttons.cancel"),
color: this.theme === "dark" ? "white" : "dark"
},
})
}
passwordPromise
.then(() => {
this.$q.loading.show({
delay: 0
})
this.$gateway.send("wallet", "create_wallet", this.wallet)
})
.catch(() => {})
},
cancel() {
this.$router.replace({ path: "/wallet-select" });
}
},
components: {
LokiField
},
deep: true
}
}
},
validations: {
wallet: {
name: { required }
}
},
methods: {
create() {
this.$v.wallet.$touch();
if (this.$v.wallet.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.enterWalletName")
});
return;
}
if (this.wallet.password != this.wallet.password_confirm) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.passwordNoMatch")
});
return;
}
// Warn user if no password is set
let passwordPromise = Promise.resolve();
if (!this.wallet.password) {
passwordPromise = this.$q.dialog({
title: this.$t("dialog.noPassword.title"),
message: this.$t("dialog.noPassword.message"),
ok: {
label: this.$t("dialog.noPassword.ok")
},
cancel: {
flat: true,
label: this.$t("dialog.buttons.cancel"),
color: this.theme === "dark" ? "white" : "dark"
}
});
}
passwordPromise
.then(() => {
this.$q.loading.show({
delay: 0
});
this.$gateway.send("wallet", "create_wallet", this.wallet);
})
.catch(() => {});
},
cancel() {
this.$router.replace({ path: "/wallet-select" });
}
}
};
</script>
<style lang="scss">
.create-wallet {
.fields {
> * {
margin-bottom: 16px;
}
.fields {
> * {
margin-bottom: 16px;
}
}
}
</style>

View File

@ -1,211 +1,216 @@
<template>
<q-page padding class="created">
<q-page padding class="created">
<div class="col wallet q-mb-lg">
<h6>{{walletName}}</h6>
<div class="row items-center">
<div class="col address">
{{ info.address }}
</div>
<div class="q-item-side">
<q-btn
color="primary" style="width:25px;"
size="sm" icon="file_copy"
@click="copyAddress">
<q-tooltip anchor="center left" self="center right" :offset="[5, 10]">
{{ $t("menuItems.copyAddress") }}
</q-tooltip>
</q-btn>
</div>
<h6>{{ walletName }}</h6>
<div class="row items-center">
<div class="col address">
{{ info.address }}
</div>
<div class="q-item-side">
<q-btn color="primary" style="width:25px;" size="sm" icon="file_copy" @click="copyAddress">
<q-tooltip anchor="center left" self="center right" :offset="[5, 10]">
{{ $t("menuItems.copyAddress") }}
</q-tooltip>
</q-btn>
</div>
</div>
</div>
<template v-if="secret.mnemonic">
<div class="seed-box col">
<h6 class="q-mb-xs q-mt-lg">{{ $t("strings.seedWords") }}</h6>
<div class="seed q-my-lg">
{{ secret.mnemonic }}
</div>
<div class="q-my-md warning">
{{ $t("strings.saveSeedWarning") }}
</div>
<div>
<q-btn
color="primary"
size="md"
icon="file_copy"
label="Copy seed words"
@click="copyPrivateKey('mnemonic', $event)"
/>
</div>
<div class="seed-box col">
<h6 class="q-mb-xs q-mt-lg">{{ $t("strings.seedWords") }}</h6>
<div class="seed q-my-lg">
{{ secret.mnemonic }}
</div>
<div class="q-my-md warning">
{{ $t("strings.saveSeedWarning") }}
</div>
<div>
<q-btn
color="primary"
size="md"
icon="file_copy"
label="Copy seed words"
@click="copyPrivateKey('mnemonic', $event)"
/>
</div>
</div>
</template>
<q-collapsible label="Advanced" header-class="q-mt-sm non-selectable row reverse advanced-options-label">
<template v-if="secret.view_key != secret.spend_key">
<h6 class="q-mb-xs title">{{ $t("strings.viewKey") }}</h6>
<div class="row">
<div class="col" style="word-break:break-all;">
{{ secret.view_key }}
</div>
<div class="q-item-side">
<q-btn
color="primary" style="width:25px;"
size="sm" icon="file_copy"
@click="copyPrivateKey('view_key', $event)">
<q-tooltip anchor="center left" self="center right" :offset="[5, 10]">
{{ $t("menuItems.copyViewKey") }}
</q-tooltip>
</q-btn>
</div>
</div>
</template>
<template v-if="secret.view_key != secret.spend_key">
<h6 class="q-mb-xs title">{{ $t("strings.viewKey") }}</h6>
<div class="row">
<div class="col" style="word-break:break-all;">
{{ secret.view_key }}
</div>
<div class="q-item-side">
<q-btn
color="primary"
style="width:25px;"
size="sm"
icon="file_copy"
@click="copyPrivateKey('view_key', $event)"
>
<q-tooltip anchor="center left" self="center right" :offset="[5, 10]">
{{ $t("menuItems.copyViewKey") }}
</q-tooltip>
</q-btn>
</div>
</div>
</template>
<template v-if="!/^0*$/.test(secret.spend_key)">
<h6 class="q-mb-xs title">{{ $t("strings.spendKey") }}</h6>
<div class="row">
<div class="col" style="word-break:break-all;">
{{ secret.spend_key }}
</div>
<div class="q-item-side">
<q-btn
color="primary" style="width:25px;"
size="sm" icon="file_copy"
@click="copyPrivateKey('spend_key', $event)">
<q-tooltip anchor="center left" self="center right" :offset="[5, 10]">
{{ $t("menuItems.copySpendKey") }}
</q-tooltip>
</q-btn>
</div>
</div>
</template>
<template v-if="!/^0*$/.test(secret.spend_key)">
<h6 class="q-mb-xs title">{{ $t("strings.spendKey") }}</h6>
<div class="row">
<div class="col" style="word-break:break-all;">
{{ secret.spend_key }}
</div>
<div class="q-item-side">
<q-btn
color="primary"
style="width:25px;"
size="sm"
icon="file_copy"
@click="copyPrivateKey('spend_key', $event)"
>
<q-tooltip anchor="center left" self="center right" :offset="[5, 10]">
{{ $t("menuItems.copySpendKey") }}
</q-tooltip>
</q-btn>
</div>
</div>
</template>
</q-collapsible>
<q-btn class="q-mt-lg" color="primary" @click="open" :label="$t('buttons.openWallet')" />
</q-page>
<q-btn class="q-mt-lg" color="primary" :label="$t('buttons.openWallet')" @click="open" />
</q-page>
</template>
<script>
const { clipboard } = require("electron")
import { mapState } from "vuex"
import AddressHeader from "components/address_header"
const { clipboard } = require("electron");
import { mapState } from "vuex";
export default {
computed: mapState({
info: state => state.gateway.wallet.info,
secret: state => state.gateway.wallet.secret,
walletName (state) {
return `Wallet: ${this.info.name}`
}
}),
methods: {
open() {
setTimeout(() => {
this.$store.commit("gateway/set_wallet_data", {
secret: {
mnemonic: "",
spend_key: "",
view_key: ""
}
})
}, 500)
this.$router.replace({ path: "/wallet" });
},
copyPrivateKey (type, event) {
event.stopPropagation()
for(let i = 0; i < event.path.length; i++) {
if(event.path[i].tagName == "BUTTON") {
event.path[i].blur()
break
}
}
if(this.secret[type] == null) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.copyingPrivateKeys"),
})
return
}
clipboard.writeText(this.secret[type])
let type_key = "seedWords"
if (type === "spend_key") {
type_key = "spendKey"
} else if (type === "view_key") {
type_key = "viewKey"
}
const type_title = this.$t("dialog.copyPrivateKeys." + type_key)
this.$q.dialog({
title: this.$t("dialog.copyPrivateKeys.title", { type: type_title }) ,
message: this.$t("dialog.copyPrivateKeys.message"),
ok: {
label: this.$t("dialog.buttons.ok")
},
}).catch(() => null).then(() => {
this.$q.notify({
type: "positive",
timeout: 1000,
message: this.$t("notification.positive.copied", { item: this.$t("strings." + type_key) })
})
})
},
copyAddress() {
clipboard.writeText(this.info.address)
this.$q.notify({
type: "positive",
timeout: 1000,
message: this.$t("notification.positive.addressCopied")
})
}
},
components: {
AddressHeader,
computed: mapState({
info: state => state.gateway.wallet.info,
secret: state => state.gateway.wallet.secret,
walletName() {
return `Wallet: ${this.info.name}`;
}
}),
methods: {
open() {
setTimeout(() => {
this.$store.commit("gateway/set_wallet_data", {
secret: {
mnemonic: "",
spend_key: "",
view_key: ""
}
});
}, 500);
this.$router.replace({ path: "/wallet" });
},
copyPrivateKey(type, event) {
event.stopPropagation();
for (let i = 0; i < event.path.length; i++) {
if (event.path[i].tagName == "BUTTON") {
event.path[i].blur();
break;
}
}
}
if (this.secret[type] == null) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.copyingPrivateKeys")
});
return;
}
clipboard.writeText(this.secret[type]);
let type_key = "seedWords";
if (type === "spend_key") {
type_key = "spendKey";
} else if (type === "view_key") {
type_key = "viewKey";
}
const type_title = this.$t("dialog.copyPrivateKeys." + type_key);
this.$q
.dialog({
title: this.$t("dialog.copyPrivateKeys.title", {
type: type_title
}),
message: this.$t("dialog.copyPrivateKeys.message"),
ok: {
label: this.$t("dialog.buttons.ok")
}
})
.catch(() => null)
.then(() => {
this.$q.notify({
type: "positive",
timeout: 1000,
message: this.$t("notification.positive.copied", {
item: this.$t("strings." + type_key)
})
});
});
},
copyAddress() {
clipboard.writeText(this.info.address);
this.$q.notify({
type: "positive",
timeout: 1000,
message: this.$t("notification.positive.addressCopied")
});
}
}
};
</script>
<style lang="scss">
.created {
.wallet h6 {
text-align: center;
}
.wallet h6 {
text-align: center;
}
.address {
text-align: center;
word-break: break-all;
}
.address {
text-align: center;
word-break: break-all;
}
.seed-box {
border: 1px solid white;
border-radius: 3px;
margin: 16px;
padding: 16px;
.seed-box {
border: 1px solid white;
border-radius: 3px;
margin: 16px;
padding: 16px;
div, h6 {
text-align: center;
}
.seed {
font-size: 24px;
font-weight: 600;
}
.warning {
color: goldenrod;
}
}
div,
h6 {
font-size: 18px;
margin: 8px 0;
font-weight: 450;
text-align: center;
}
.advanced-options-label {
padding-left: 0;
padding-right: 0;
.seed {
font-size: 24px;
font-weight: 600;
}
.warning {
color: goldenrod;
}
}
h6 {
font-size: 18px;
margin: 8px 0;
font-weight: 450;
}
.advanced-options-label {
padding-left: 0;
padding-right: 0;
}
}
</style>

View File

@ -1,138 +1,145 @@
<template>
<q-page>
<q-page>
<div class="q-mx-md">
<template v-if="wallets_legacy.length == 2">
<q-field>
<div class="row gutter-md">
<div><q-radio v-model="legacy_type" val="0" label="Full wallet" /></div>
<div><q-radio v-model="legacy_type" val="1" label="LITE wallet" /></div>
</div>
</q-field>
</template>
<q-field class="q-mt-none">
<q-input
v-model="wallet.name"
float-label="New wallet name"
@blur="$v.wallet.name.$touch"
:error="$v.wallet.name.$error"
:dark="theme=='dark'"
/>
</q-field>
<template v-if="wallets_legacy.length == 2">
<q-field>
<div class="row gutter-sm">
<div class="col-12">
<q-input v-model="wallet_path" stack-label="Wallet file" disable :dark="theme=='dark'" />
</div>
<div class="row gutter-md">
<div>
<q-radio v-model="legacy_type" val="0" label="Full wallet" />
</div>
<div>
<q-radio v-model="legacy_type" val="1" label="LITE wallet" />
</div>
</div>
</q-field>
</template>
<q-field>
<q-input v-model="wallet.password" type="password" float-label="Password" :dark="theme=='dark'" />
</q-field>
<q-field class="q-mt-none">
<q-input
v-model="wallet.name"
float-label="New wallet name"
:error="$v.wallet.name.$error"
:dark="theme == 'dark'"
@blur="$v.wallet.name.$touch"
/>
</q-field>
<q-field>
<q-input v-model="wallet.password_confirm" type="password" float-label="Confirm Password" :dark="theme=='dark'" />
</q-field>
<q-field>
<div class="row gutter-sm">
<div class="col-12">
<q-input v-model="wallet_path" stack-label="Wallet file" disable :dark="theme == 'dark'" />
</div>
</div>
</q-field>
<q-field>
<q-btn color="primary" @click="import_wallet" label="Import wallet" />
</q-field>
<q-field>
<q-input v-model="wallet.password" type="password" float-label="Password" :dark="theme == 'dark'" />
</q-field>
<q-field>
<q-input
v-model="wallet.password_confirm"
type="password"
float-label="Confirm Password"
:dark="theme == 'dark'"
/>
</q-field>
<q-field>
<q-btn color="primary" label="Import wallet" @click="import_wallet" />
</q-field>
</div>
</q-page>
</q-page>
</template>
<script>
import { required } from "vuelidate/lib/validators"
import { mapState } from "vuex"
import { required } from "vuelidate/lib/validators";
import { mapState } from "vuex";
export default {
data () {
return {
wallet: {
name: "",
path: "",
password: "",
password_confirm: ""
},
legacy_type: "0"
}
},
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
status: state => state.gateway.wallet.status,
wallets_legacy: state => state.gateway.wallets.legacy,
wallet_path (state) {
return state.gateway.wallets.legacy[this.legacy_type].path
}
}),
watch: {
status: {
handler(val, old){
if(val.code == old.code) return
switch(this.status.code) {
case 1:
break;
case 0:
this.$q.loading.hide()
this.$router.replace({ path: "/wallet-select/created" });
break;
default:
this.$q.loading.hide()
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.status.message
})
break;
}
},
deep: true
}
},
validations: {
wallet: {
name: { required }
}
},
methods: {
import_wallet() {
this.$v.wallet.$touch()
if (this.$v.wallet.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.enterWalletName")
})
return
}
if(this.wallet.password != this.wallet.password_confirm) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.passwordNoMatch")
})
return
}
this.$q.loading.show({
delay: 0
})
this.wallet.path = this.wallet_path
this.$gateway.send("wallet", "import_wallet", this.wallet);
},
cancel() {
this.$router.replace({ path: "/wallet-select" });
}
data() {
return {
wallet: {
name: "",
path: "",
password: "",
password_confirm: ""
},
legacy_type: "0"
};
},
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
status: state => state.gateway.wallet.status,
wallets_legacy: state => state.gateway.wallets.legacy,
wallet_path(state) {
return state.gateway.wallets.legacy[this.legacy_type].path;
}
}
}),
watch: {
status: {
handler(val, old) {
if (val.code == old.code) return;
switch (this.status.code) {
case 1:
break;
case 0:
this.$q.loading.hide();
this.$router.replace({
path: "/wallet-select/created"
});
break;
default:
this.$q.loading.hide();
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.status.message
});
break;
}
},
deep: true
}
},
validations: {
wallet: {
name: { required }
}
},
methods: {
import_wallet() {
this.$v.wallet.$touch();
if (this.$v.wallet.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.enterWalletName")
});
return;
}
if (this.wallet.password != this.wallet.password_confirm) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.passwordNoMatch")
});
return;
}
this.$q.loading.show({
delay: 0
});
this.wallet.path = this.wallet_path;
this.$gateway.send("wallet", "import_wallet", this.wallet);
},
cancel() {
this.$router.replace({ path: "/wallet-select" });
}
}
};
</script>
<style>
</style>
<style></style>

View File

@ -1,139 +1,144 @@
<template>
<q-page>
<q-page>
<div class="q-mx-md import-old-gui">
<q-list link dark no-border class="wallet-list">
<q-item v-for="state in directory_state" :key="state.directory" :class="{selected : state.selected}">
<q-item-side>
<q-checkbox v-model="state.selected" />
</q-item-side>
<q-item-main @click.native="state.selected = !state.selected">
<q-item-tile label>{{ state.directory }}</q-item-tile>
</q-item-main>
<q-item-side>
<q-select hide-underline dark class="q-ma-none full-width" v-model="state.type" :options="selectOptions" />
</q-item-side>
</q-item>
</q-list>
<q-list link dark no-border class="wallet-list">
<q-item v-for="state in directory_state" :key="state.directory" :class="{ selected: state.selected }">
<q-item-side>
<q-checkbox v-model="state.selected" />
</q-item-side>
<q-item-main @click.native="state.selected = !state.selected">
<q-item-tile label>{{ state.directory }}</q-item-tile>
</q-item-main>
<q-item-side>
<q-select v-model="state.type" hide-underline dark class="q-ma-none full-width" :options="selectOptions" />
</q-item-side>
</q-item>
</q-list>
<q-field>
<q-btn color="primary" @click="import_wallets" :label="$tc('buttons.importWallet', 2)" :disable="selectedWallets.length === 0"/>
</q-field>
<q-field>
<q-btn
color="primary"
:label="$tc('buttons.importWallet', 2)"
:disable="selectedWallets.length === 0"
@click="import_wallets"
/>
</q-field>
</div>
</q-page>
</q-page>
</template>
<script>
import { mapState } from "vuex"
import { mapState } from "vuex";
export default {
data () {
return {
directory_state: [],
}
data() {
return {
directory_state: []
};
},
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
directories: state => state.gateway.wallets.directories,
old_gui_import_status: state => state.gateway.old_gui_import_status,
selectOptions: () => [
{
label: "Main",
value: "mainnet"
},
{
label: "Staging",
value: "stagenet"
},
{
label: "Test",
value: "testnet"
}
],
selectedWallets() {
return this.directory_state.filter(s => s.selected);
}
}),
watch: {
directories: {
handler() {
this.populate_state();
},
deep: true
},
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
directories: state => state.gateway.wallets.directories,
old_gui_import_status: state => state.gateway.old_gui_import_status,
selectOptions: state => [
{
label: 'Main',
value: 'mainnet'
},
{
label: 'Staging',
value: 'stagenet'
},
{
label: 'Test',
value: 'testnet'
},
],
selectedWallets () {
return this.directory_state.filter(s => s.selected)
}
}),
watch: {
directories: {
handler(val, old) {
this.populate_state()
},
deep: true
},
old_gui_import_status: {
handler(val, old) {
if(val.code == old.code) return
old_gui_import_status: {
handler(val, old) {
if (val.code == old.code) return;
const { code, failed_wallets } = this.old_gui_import_status
const { code, failed_wallets } = this.old_gui_import_status;
// Imported
if (code === 0) {
this.$q.loading.hide()
if (failed_wallets.length === 0) {
this.$router.replace({ path: "/wallet-select" });
} else {
failed_wallets.forEach(wallet => {
this.$q.notify({
type: "negative",
timeout: 3000,
message: this.$t("notification.errors.failedWalletImport") + `: ${wallet}`
})
})
}
}
},
deep: true
}
},
created () {
this.$gateway.send("wallet", "list_wallets")
this.populate_state()
},
methods: {
populate_state () {
// Keep any directories that intersect
const new_state = this.directory_state.filter(state => this.directories.includes(state.directory))
// Add in new directories
this.directories
.filter(dir => !new_state.find(state => state.directory === dir))
.forEach(directory => {
new_state.push({
directory,
selected: false,
type: "mainnet"
})
});
// Sort them
this.directory_state = new_state.sort(function(a, b){
return a.directory.localeCompare(b.directory);
})
},
import_wallets() {
this.$q.loading.show({
delay: 0
})
this.$gateway.send("wallet", "copy_old_gui_wallets", {
wallets: this.selectedWallets
})
},
cancel() {
// Imported
if (code === 0) {
this.$q.loading.hide();
if (failed_wallets.length === 0) {
this.$router.replace({ path: "/wallet-select" });
} else {
failed_wallets.forEach(wallet => {
this.$q.notify({
type: "negative",
timeout: 3000,
message: this.$t("notification.errors.failedWalletImport") + `: ${wallet}`
});
});
}
}
},
deep: true
}
},
created() {
this.$gateway.send("wallet", "list_wallets");
this.populate_state();
},
methods: {
populate_state() {
// Keep any directories that intersect
const new_state = this.directory_state.filter(state => this.directories.includes(state.directory));
// Add in new directories
this.directories
.filter(dir => !new_state.find(state => state.directory === dir))
.forEach(directory => {
new_state.push({
directory,
selected: false,
type: "mainnet"
});
});
// Sort them
this.directory_state = new_state.sort(function(a, b) {
return a.directory.localeCompare(b.directory);
});
},
}
import_wallets() {
this.$q.loading.show({
delay: 0
});
this.$gateway.send("wallet", "copy_old_gui_wallets", {
wallets: this.selectedWallets
});
},
cancel() {
this.$router.replace({ path: "/wallet-select" });
}
}
};
</script>
<style lang="scss">
.import-old-gui {
.wallet-list {
.q-item {
margin: 10px 0px;
margin-bottom: 0px;
padding: 14px;
border-radius: 3px;
}
.wallet-list {
.q-item {
margin: 10px 0px;
margin-bottom: 0px;
padding: 14px;
border-radius: 3px;
}
}
}
</style>

View File

@ -1,217 +1,239 @@
<template>
<q-page>
<q-page>
<div class="q-mx-md">
<q-field class="q-mt-none">
<q-input
v-model="wallet.name"
float-label="Wallet name"
@blur="$v.wallet.name.$touch"
:error="$v.wallet.name.$error"
:dark="theme=='dark'"
/>
</q-field>
<q-field class="q-mt-none">
<q-input
v-model="wallet.name"
float-label="Wallet name"
:error="$v.wallet.name.$error"
:dark="theme == 'dark'"
@blur="$v.wallet.name.$touch"
/>
</q-field>
<q-field>
<q-input
v-model="wallet.address"
float-label="Wallet address"
@blur="$v.wallet.address.$touch"
:error="$v.wallet.address.$error"
:dark="theme=='dark'"
/>
</q-field>
<q-field>
<q-input
v-model="wallet.address"
float-label="Wallet address"
:error="$v.wallet.address.$error"
:dark="theme == 'dark'"
@blur="$v.wallet.address.$touch"
/>
</q-field>
<q-field>
<q-input
v-model="wallet.viewkey"
float-label="Private viewkey"
@blur="$v.wallet.viewkey.$touch"
:error="$v.wallet.viewkey.$error"
:dark="theme=='dark'"
/>
</q-field>
<q-field>
<q-input
v-model="wallet.viewkey"
float-label="Private viewkey"
:error="$v.wallet.viewkey.$error"
:dark="theme == 'dark'"
@blur="$v.wallet.viewkey.$touch"
/>
</q-field>
<q-field>
<div class="row items-center gutter-sm">
<div class="col">
<template v-if="wallet.refresh_type=='date'">
<q-datetime v-model="wallet.refresh_start_date" type="date"
float-label="Restore from date"
modal :min="1492486495000" :max="Date.now()"
:dark="theme=='dark'"
/>
</template>
<template v-else-if="wallet.refresh_type=='height'">
<q-input v-model="wallet.refresh_start_height" type="number"
min="0" float-label="Restore from block height"
@blur="$v.wallet.refresh_start_height.$touch"
:error="$v.wallet.refresh_start_height.$error"
:dark="theme=='dark'"
/>
</template>
<q-field>
<div class="row items-center gutter-sm">
<div class="col">
<template v-if="wallet.refresh_type == 'date'">
<q-datetime
v-model="wallet.refresh_start_date"
type="date"
float-label="Restore from date"
modal
:min="1492486495000"
:max="Date.now()"
:dark="theme == 'dark'"
/>
</template>
<template v-else-if="wallet.refresh_type == 'height'">
<q-input
v-model="wallet.refresh_start_height"
type="number"
min="0"
float-label="Restore from block height"
:error="$v.wallet.refresh_start_height.$error"
:dark="theme == 'dark'"
@blur="$v.wallet.refresh_start_height.$touch"
/>
</template>
</div>
<div class="col-auto">
<template v-if="wallet.refresh_type == 'date'">
<q-btn
class="float-right"
:text-color="theme == 'dark' ? 'white' : 'dark'"
flat
@click="wallet.refresh_type = 'height'"
>
<div style="width: 80px;" class="text-center">
<q-icon class="block" name="clear_all" />
<div style="font-size:10px">Switch to<br />height select</div>
</div>
<div class="col-auto">
<template v-if="wallet.refresh_type=='date'">
<q-btn @click="wallet.refresh_type='height'" class="float-right" :text-color="theme=='dark'?'white':'dark'" flat>
<div style="width: 80px;" class="text-center">
<q-icon class="block" name="clear_all" />
<div style="font-size:10px">Switch to<br/>height select</div>
</div>
</q-btn>
</template>
<template v-else-if="wallet.refresh_type=='height'">
<q-btn @click="wallet.refresh_type='date'" class="float-right" :text-color="theme=='dark'?'white':'dark'" flat>
<div style="width: 80px;" class="text-center">
<q-icon class="block" name="today" />
<div style="font-size:10px">Switch to<br/>date select</div>
</div>
</q-btn>
</template>
</q-btn>
</template>
<template v-else-if="wallet.refresh_type == 'height'">
<q-btn
class="float-right"
:text-color="theme == 'dark' ? 'white' : 'dark'"
flat
@click="wallet.refresh_type = 'date'"
>
<div style="width: 80px;" class="text-center">
<q-icon class="block" name="today" />
<div style="font-size:10px">Switch to<br />date select</div>
</div>
</div>
</q-field>
</q-btn>
</template>
</div>
</div>
</q-field>
<q-field>
<q-input v-model="wallet.password" type="password" float-label="Password" :dark="theme=='dark'" />
</q-field>
<q-field>
<q-input v-model="wallet.password" type="password" float-label="Password" :dark="theme == 'dark'" />
</q-field>
<q-field>
<q-input v-model="wallet.password_confirm" type="password" float-label="Confirm Password" :dark="theme=='dark'" />
</q-field>
<q-field>
<q-btn color="primary" @click="restore_view_wallet" label="Restore view-only wallet" />
</q-field>
<q-field>
<q-input
v-model="wallet.password_confirm"
type="password"
float-label="Confirm Password"
:dark="theme == 'dark'"
/>
</q-field>
<q-field>
<q-btn color="primary" label="Restore view-only wallet" @click="restore_view_wallet" />
</q-field>
</div>
</q-page>
</q-page>
</template>
<script>
import { required, numeric } from "vuelidate/lib/validators"
import { privkey, address } from "src/validators/common"
import { mapState } from "vuex"
import { required, numeric } from "vuelidate/lib/validators";
import { privkey, address } from "src/validators/common";
import { mapState } from "vuex";
export default {
data () {
return {
wallet: {
name: "",
address: "",
viewkey: "",
refresh_type: "date",
refresh_start_height: 0,
refresh_start_date: 1492486495000, // timestamp of block 1
password: "",
password_confirm: ""
},
}
},
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
status: state => state.gateway.wallet.status,
}),
watch: {
status: {
handler(val, old){
if(val.code == old.code) return
switch(this.status.code) {
case 1:
break;
case 0:
this.$q.loading.hide()
this.$router.replace({ path: "/wallet-select/created" });
break;
default:
this.$q.loading.hide()
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.status.message
})
break;
}
},
deep: true
}
},
validations: {
wallet: {
name: { required },
address: {
required,
isAddress(value) {
if (value === '') return true
return new Promise(resolve => {
address(value, this.$gateway)
.then(() => resolve(true))
.catch(e => resolve(false))
});
}
},
viewkey: { required, privkey },
refresh_start_height: { numeric }
}
},
methods: {
restore_view_wallet() {
this.$v.wallet.$touch()
if (this.$v.wallet.name.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.enterWalletName")
})
return
}
if (this.$v.wallet.address.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.invalidPublicAddress")
})
return
}
if (this.$v.wallet.viewkey.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.invalidPrivateViewKey")
})
return
}
if (this.$v.wallet.refresh_start_height.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.invalidRestoreHeight")
})
return
}
if(this.wallet.password != this.wallet.password_confirm) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.passwordNoMatch")
})
return
}
this.$q.loading.show({
delay: 0
})
this.$gateway.send("wallet", "restore_view_wallet", this.wallet);
},
cancel() {
this.$router.replace({ path: "/wallet-select" });
data() {
return {
wallet: {
name: "",
address: "",
viewkey: "",
refresh_type: "date",
refresh_start_height: 0,
refresh_start_date: 1492486495000, // timestamp of block 1
password: "",
password_confirm: ""
}
};
},
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
status: state => state.gateway.wallet.status
}),
watch: {
status: {
handler(val, old) {
if (val.code == old.code) return;
switch (this.status.code) {
case 1:
break;
case 0:
this.$q.loading.hide();
this.$router.replace({
path: "/wallet-select/created"
});
break;
default:
this.$q.loading.hide();
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.status.message
});
break;
}
},
deep: true
}
}
},
validations: {
wallet: {
name: { required },
address: {
required,
isAddress(value) {
if (value === "") return true;
return new Promise(resolve => {
address(value, this.$gateway)
.then(() => resolve(true))
.catch(() => resolve(false));
});
}
},
viewkey: { required, privkey },
refresh_start_height: { numeric }
}
},
methods: {
restore_view_wallet() {
this.$v.wallet.$touch();
if (this.$v.wallet.name.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.enterWalletName")
});
return;
}
if (this.$v.wallet.address.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.invalidPublicAddress")
});
return;
}
if (this.$v.wallet.viewkey.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.invalidPrivateViewKey")
});
return;
}
if (this.$v.wallet.refresh_start_height.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.invalidRestoreHeight")
});
return;
}
if (this.wallet.password != this.wallet.password_confirm) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.passwordNoMatch")
});
return;
}
this.$q.loading.show({
delay: 0
});
this.$gateway.send("wallet", "restore_view_wallet", this.wallet);
},
cancel() {
this.$router.replace({ path: "/wallet-select" });
}
}
};
</script>
<style>
</style>
<style></style>

View File

@ -1,178 +1,177 @@
<template>
<q-page>
<q-page>
<div class="q-mx-md import-wallet">
<LokiField :label="$t('fieldLabels.newWalletName')" :error="$v.wallet.name.$error">
<q-input
v-model="wallet.name"
:placeholder="$t('placeholders.walletName')"
:dark="theme == 'dark'"
hide-underline
@keyup.enter="import_wallet"
@blur="$v.wallet.name.$touch"
/>
</LokiField>
<LokiField :label="$t('fieldLabels.newWalletName')" :error="$v.wallet.name.$error">
<q-input
v-model="wallet.name"
:placeholder="$t('placeholders.walletName')"
@keyup.enter="import_wallet"
@blur="$v.wallet.name.$touch"
:dark="theme=='dark'"
hide-underline
/>
</LokiField>
<LokiField :label="$t('fieldLabels.walletFile')" disable-hover :error="$v.wallet.path.$error">
<q-input
v-model="wallet.path"
:placeholder="$t('placeholders.selectAFile')"
disable
:dark="theme == 'dark'"
hide-underline
/>
<input id="walletPath" ref="fileInput" type="file" hidden @change="setWalletPath" />
<q-btn
color="secondary"
:label="$t('buttons.selectWalletFile')"
:text-color="theme == 'dark' ? 'white' : 'dark'"
@click="selectFile"
/>
</LokiField>
<LokiField :label="$t('fieldLabels.walletFile')" disable-hover :error="$v.wallet.path.$error">
<q-input
v-model="wallet.path"
:placeholder="$t('placeholders.selectAFile')"
disable
:dark="theme=='dark'"
hide-underline
/>
<input type="file" id="walletPath" v-on:change="setWalletPath" ref="fileInput" hidden />
<q-btn
color="secondary"
:label="$t('buttons.selectWalletFile')"
v-on:click="selectFile"
:text-color="theme=='dark'?'white':'dark'"
/>
</LokiField>
<LokiField :label="$t('fieldLabels.password')">
<q-input
v-model="wallet.password"
:placeholder="$t('placeholders.walletPassword')"
type="password"
:dark="theme == 'dark'"
hide-underline
@keyup.enter="import_wallet"
/>
</LokiField>
<LokiField :label="$t('fieldLabels.password')">
<q-input
v-model="wallet.password"
:placeholder="$t('placeholders.walletPassword')"
@keyup.enter="import_wallet"
type="password"
:dark="theme=='dark'"
hide-underline
/>
</LokiField>
<LokiField :label="$t('fieldLabels.confirmPassword')">
<q-input
v-model="wallet.password_confirm"
@keyup.enter="import_wallet"
type="password"
:dark="theme=='dark'"
hide-underline
/>
</LokiField>
<q-field>
<q-btn color="primary" @click="import_wallet" :label="$tc('buttons.importWallet', 1)" />
</q-field>
<LokiField :label="$t('fieldLabels.confirmPassword')">
<q-input
v-model="wallet.password_confirm"
type="password"
:dark="theme == 'dark'"
hide-underline
@keyup.enter="import_wallet"
/>
</LokiField>
<q-field>
<q-btn color="primary" :label="$tc('buttons.importWallet', 1)" @click="import_wallet" />
</q-field>
</div>
</q-page>
</q-page>
</template>
<script>
import { required } from "vuelidate/lib/validators"
import { mapState } from "vuex"
import LokiField from "components/loki_field"
import { required } from "vuelidate/lib/validators";
import { mapState } from "vuex";
import LokiField from "components/loki_field";
export default {
data () {
return {
wallet: {
name: "",
path: "",
password: "",
password_confirm: ""
},
components: {
LokiField
},
data() {
return {
wallet: {
name: "",
path: "",
password: "",
password_confirm: ""
}
};
},
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
status: state => state.gateway.wallet.status
}),
watch: {
status: {
handler(val, old) {
if (val.code == old.code) return;
switch (this.status.code) {
case 1:
break;
case 0:
this.$q.loading.hide();
this.$router.replace({
path: "/wallet-select/created"
});
break;
default:
this.$q.loading.hide();
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.status.message
});
break;
}
},
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
status: state => state.gateway.wallet.status,
}),
watch: {
status: {
handler(val, old){
if(val.code == old.code) return
switch(this.status.code) {
case 1:
break;
case 0:
this.$q.loading.hide()
this.$router.replace({ path: "/wallet-select/created" });
break;
default:
this.$q.loading.hide()
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.status.message
})
break;
}
},
deep: true
}
},
validations: {
wallet: {
name: { required },
path: { required }
}
},
methods: {
selectFile () {
this.$refs.fileInput.click()
},
setWalletPath (file) {
this.wallet.path = file.target.files[0].path
},
import_wallet() {
this.$v.wallet.$touch()
if (this.$v.wallet.name.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.enterWalletName")
})
return
}
if (this.$v.wallet.path.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.selectWalletFile")
})
return
}
if (this.wallet.password != this.wallet.password_confirm) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.passwordNoMatch")
})
return
}
this.$q.loading.show({
delay: 0
})
this.$gateway.send("wallet", "import_wallet", this.wallet);
},
cancel() {
this.$router.replace({ path: "/wallet-select" });
}
},
components: {
LokiField
},
deep: true
}
}
},
validations: {
wallet: {
name: { required },
path: { required }
}
},
methods: {
selectFile() {
this.$refs.fileInput.click();
},
setWalletPath(file) {
this.wallet.path = file.target.files[0].path;
},
import_wallet() {
this.$v.wallet.$touch();
if (this.$v.wallet.name.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.enterWalletName")
});
return;
}
if (this.$v.wallet.path.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.selectWalletFile")
});
return;
}
if (this.wallet.password != this.wallet.password_confirm) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.passwordNoMatch")
});
return;
}
this.$q.loading.show({
delay: 0
});
this.$gateway.send("wallet", "import_wallet", this.wallet);
},
cancel() {
this.$router.replace({ path: "/wallet-select" });
}
}
};
</script>
<style lang="scss">
.import-wallet {
.q-if-disabled {
cursor: default !important;
.q-input-target {
cursor: default !important;
}
.q-if-disabled {
cursor: default !important;
.q-input-target {
cursor: default !important;
}
}
.loki-field {
margin-top: 16px;
}
.loki-field {
margin-top: 16px;
}
}
</style>

View File

@ -1,229 +1,258 @@
<template>
<q-page>
<q-list class="wallet-list" link no-border :dark="theme=='dark'">
<template v-if="wallets.list.length">
<div class="header row justify-between items-center">
<div class="header-title">{{ $t("titles.yourWallets") }}</div>
<q-btn class="add" icon="add" size="md" color="primary" v-if="wallets.list.length">
<q-popover class="header-popover">
<q-list separator link>
<q-item v-for="action in actions" @click.native="action.handler" :key="action.name">
<q-item-main :label="action.name" />
</q-item>
</q-list>
</q-popover>
</q-btn>
<q-page>
<q-list class="wallet-list" link no-border :dark="theme == 'dark'">
<template v-if="wallets.list.length">
<div class="header row justify-between items-center">
<div class="header-title">
{{ $t("titles.yourWallets") }}
</div>
<q-btn v-if="wallets.list.length" class="add" icon="add" size="md" color="primary">
<q-popover class="header-popover">
<q-list separator link>
<q-item v-for="action in actions" :key="action.name" @click.native="action.handler">
<q-item-main :label="action.name" />
</q-item>
</q-list>
</q-popover>
</q-btn>
</div>
<div class="hr-separator" />
<q-item
v-for="wallet in wallets.list"
:key="`${wallet.address}-${wallet.name}`"
@click.native="openWallet(wallet)"
>
<q-item-side>
<div class="wallet-icon">
<svg
width="48"
viewBox="0 0 17 16"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
class="si-glyph si-glyph-wallet"
>
<defs class="si-glyph-fill"></defs>
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g transform="translate(1.000000, 0.000000)" fill="#434343">
<path
d="M7.988,10.635 L7.988,8.327 C7.988,7.578 8.561,6.969 9.267,6.969 L13.964,6.969 L13.964,5.531 C13.964,4.849 13.56,4.279 13.007,4.093 L13.007,4.094 L11.356,4.08 L11.336,4.022 L3.925,4.022 L3.784,4.07 L1.17,4.068 L1.165,4.047 C0.529,4.167 0.017,4.743 0.017,5.484 L0.017,13.437 C0.017,14.269 0.665,14.992 1.408,14.992 L12.622,14.992 C13.365,14.992 13.965,14.316 13.965,13.484 L13.965,12.031 L9.268,12.031 C8.562,12.031 7.988,11.384 7.988,10.635 L7.988,10.635 Z"
class="si-glyph-fill"
></path>
<path
d="M14.996,8.061 L14.947,8.061 L9.989,8.061 C9.46,8.061 9.031,8.529 9.031,9.106 L9.031,9.922 C9.031,10.498 9.46,10.966 9.989,10.966 L14.947,10.966 L14.996,10.966 C15.525,10.966 15.955,10.498 15.955,9.922 L15.955,9.106 C15.955,8.528 15.525,8.061 14.996,8.061 L14.996,8.061 Z M12.031,10.016 L9.969,10.016 L9.969,9 L12.031,9 L12.031,10.016 L12.031,10.016 Z"
class="si-glyph-fill"
></path>
<path
d="M3.926,4.022 L10.557,1.753 L11.337,4.022 L12.622,4.022 C12.757,4.022 12.885,4.051 13.008,4.092 L11.619,0.051 L1.049,3.572 L1.166,4.048 C1.245,4.033 1.326,4.023 1.408,4.023 L3.926,4.023 L3.926,4.022 Z"
class="si-glyph-fill"
></path>
</g>
</g>
</svg>
</div>
<div class="hr-separator" />
<q-item v-for="wallet in wallets.list" @click.native="openWallet(wallet)" :key="`${wallet.address}-${wallet.name}`">
<q-item-side>
<div class="wallet-icon">
<svg width="48" viewBox="0 0 17 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="si-glyph si-glyph-wallet"><defs class="si-glyph-fill"></defs><g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><g transform="translate(1.000000, 0.000000)" fill="#434343"><path d="M7.988,10.635 L7.988,8.327 C7.988,7.578 8.561,6.969 9.267,6.969 L13.964,6.969 L13.964,5.531 C13.964,4.849 13.56,4.279 13.007,4.093 L13.007,4.094 L11.356,4.08 L11.336,4.022 L3.925,4.022 L3.784,4.07 L1.17,4.068 L1.165,4.047 C0.529,4.167 0.017,4.743 0.017,5.484 L0.017,13.437 C0.017,14.269 0.665,14.992 1.408,14.992 L12.622,14.992 C13.365,14.992 13.965,14.316 13.965,13.484 L13.965,12.031 L9.268,12.031 C8.562,12.031 7.988,11.384 7.988,10.635 L7.988,10.635 Z" class="si-glyph-fill"></path><path d="M14.996,8.061 L14.947,8.061 L9.989,8.061 C9.46,8.061 9.031,8.529 9.031,9.106 L9.031,9.922 C9.031,10.498 9.46,10.966 9.989,10.966 L14.947,10.966 L14.996,10.966 C15.525,10.966 15.955,10.498 15.955,9.922 L15.955,9.106 C15.955,8.528 15.525,8.061 14.996,8.061 L14.996,8.061 Z M12.031,10.016 L9.969,10.016 L9.969,9 L12.031,9 L12.031,10.016 L12.031,10.016 Z" class="si-glyph-fill"></path><path d="M3.926,4.022 L10.557,1.753 L11.337,4.022 L12.622,4.022 C12.757,4.022 12.885,4.051 13.008,4.092 L11.619,0.051 L1.049,3.572 L1.166,4.048 C1.245,4.033 1.326,4.023 1.408,4.023 L3.926,4.023 L3.926,4.022 Z" class="si-glyph-fill"></path></g></g></svg>
</div>
</q-item-side>
<q-item-main>
<q-item-tile label>{{ wallet.name }}</q-item-tile>
<q-item-tile class="monospace ellipsis" sublabel>{{ wallet.address }}</q-item-tile>
</q-item-main>
</q-item-side>
<q-item-main>
<q-item-tile label>{{ wallet.name }}</q-item-tile>
<q-item-tile class="monospace ellipsis" sublabel>{{ wallet.address }}</q-item-tile>
</q-item-main>
<q-context-menu>
<q-list link separator style="min-width: 150px; max-height: 300px;">
<q-item v-close-overlay
@click.native="openWallet(wallet)">
<q-item-main :label="$t('menuItems.openWallet')" />
</q-item>
<q-context-menu>
<q-list link separator style="min-width: 150px; max-height: 300px;">
<q-item v-close-overlay @click.native="openWallet(wallet)">
<q-item-main :label="$t('menuItems.openWallet')" />
</q-item>
<q-item v-close-overlay
@click.native="copyAddress(wallet.address, $event)">
<q-item-main :label="$t('menuItems.copyAddress')" />
</q-item>
</q-list>
</q-context-menu>
</q-item>
<q-item-separator />
</template>
<template v-else>
<q-item v-for="action in actions" @click.native="action.handler" :key="action.name">
<q-item-main :label="action.name" />
</q-item>
</template>
<q-item v-close-overlay @click.native="copyAddress(wallet.address, $event)">
<q-item-main :label="$t('menuItems.copyAddress')" />
</q-item>
</q-list>
</q-context-menu>
</q-item>
<q-item-separator />
</template>
<template v-else>
<q-item v-for="action in actions" :key="action.name" @click.native="action.handler">
<q-item-main :label="action.name" />
</q-item>
</template>
</q-list>
</q-page>
</q-page>
</template>
<script>
const { clipboard } = require("electron")
import { mapState } from "vuex"
import Identicon from "components/identicon"
const { clipboard } = require("electron");
import { mapState } from "vuex";
export default {
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
wallets: state => state.gateway.wallets,
status: state => state.gateway.wallet.status,
actions (status) {
// TODO: Add this in once LOKI has the functionality
// <q-item @click.native="restoreViewWallet()">
// <q-item-main label="Restore view-only wallet" />
// </q-item>
const actions = [
{
name: this.$t("titles.wallet.createNew"),
handler: this.createNewWallet,
},
{
name: this.$t("titles.wallet.restoreFromSeed"),
handler: this.restoreWallet,
},
{
name: this.$t("titles.wallet.importFromFile"),
handler: this.importWallet,
}
];
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
wallets: state => state.gateway.wallets,
status: state => state.gateway.wallet.status,
actions() {
// TODO: Add this in once LOKI has the functionality
// <q-item @click.native="restoreViewWallet()">
// <q-item-main label="Restore view-only wallet" />
// </q-item>
const actions = [
{
name: this.$t("titles.wallet.createNew"),
handler: this.createNewWallet
},
{
name: this.$t("titles.wallet.restoreFromSeed"),
handler: this.restoreWallet
},
{
name: this.$t("titles.wallet.importFromFile"),
handler: this.importWallet
}
];
if (this.wallets.directories.length > 0) {
actions.push( {
name: this.$t("titles.wallet.importFromOldGUI"),
handler: this.importOldGuiWallets,
})
}
if (this.wallets.directories.length > 0) {
actions.push({
name: this.$t("titles.wallet.importFromOldGUI"),
handler: this.importOldGuiWallets
});
}
return actions
}
}),
created () {
this.$gateway.send("wallet", "list_wallets")
},
methods: {
openWallet(wallet) {
if(wallet.password_protected !== false) {
this.$q.dialog({
title: this.$t("dialog.password.title"),
message: this.$t("dialog.password.message"),
prompt: {
model: "",
type: "password"
},
ok: {
label: this.$t("dialog.buttons.open")
},
cancel: {
flat: true,
label: this.$t("dialog.buttons.cancel"),
color: this.theme=="dark"?"white":"dark"
}
}).then(password => {
this.$q.loading.show({
delay: 0
})
this.$gateway.send("wallet", "open_wallet", {name: wallet.name, password: password});
})
.catch(() => {
})
} else {
this.$q.loading.show({
delay: 0
})
this.$gateway.send("wallet", "open_wallet", {name: wallet.name, password: ""});
}
},
createNewWallet() {
this.$router.replace({ path: "wallet-select/create" });
},
restoreWallet() {
this.$router.replace({ path: "wallet-select/restore" });
},
restoreViewWallet() {
this.$router.replace({ path: "wallet-select/import-view-only" });
},
importWallet() {
this.$router.replace({ path: "wallet-select/import" });
},
importOldGuiWallets() {
this.$router.replace({ path: "wallet-select/import-old-gui" });
},
importLegacyWallet() {
this.$router.replace({ path: "wallet-select/import-legacy" });
},
copyAddress (address, event) {
event.stopPropagation()
for(let i = 0; i < event.path.length; i++) {
if(event.path[i].tagName == "BUTTON") {
event.path[i].blur()
break
}
}
clipboard.writeText(address)
this.$q.notify({
type: "positive",
timeout: 1000,
message: this.$t("notification.positive.addressCopied")
})
}
},
watch: {
status: {
handler(val, old){
if(val.code == old.code) return
switch(this.status.code) {
case 0: // Wallet loaded
this.$q.loading.hide()
this.$router.replace({ path: "/wallet" });
break;
case -1: // Error
case -22:
this.$q.loading.hide()
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.status.message
})
this.$store.commit("gateway/set_wallet_data", {
status: {
code: 1 // Reset to 1 (ready for action)
}
});
break;
}
},
deep: true
}
},
components: {
Identicon
return actions;
}
}
}),
watch: {
status: {
handler(val, old) {
if (val.code == old.code) return;
switch (this.status.code) {
case 0: // Wallet loaded
this.$q.loading.hide();
this.$router.replace({ path: "/wallet" });
break;
case -1: // Error
case -22:
this.$q.loading.hide();
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.status.message
});
this.$store.commit("gateway/set_wallet_data", {
status: {
code: 1 // Reset to 1 (ready for action)
}
});
break;
}
},
deep: true
}
},
created() {
this.$gateway.send("wallet", "list_wallets");
},
methods: {
openWallet(wallet) {
if (wallet.password_protected !== false) {
this.$q
.dialog({
title: this.$t("dialog.password.title"),
message: this.$t("dialog.password.message"),
prompt: {
model: "",
type: "password"
},
ok: {
label: this.$t("dialog.buttons.open")
},
cancel: {
flat: true,
label: this.$t("dialog.buttons.cancel"),
color: this.theme == "dark" ? "white" : "dark"
}
})
.then(password => {
this.$q.loading.show({
delay: 0
});
this.$gateway.send("wallet", "open_wallet", {
name: wallet.name,
password: password
});
})
.catch(() => {});
} else {
this.$q.loading.show({
delay: 0
});
this.$gateway.send("wallet", "open_wallet", {
name: wallet.name,
password: ""
});
}
},
createNewWallet() {
this.$router.replace({ path: "wallet-select/create" });
},
restoreWallet() {
this.$router.replace({ path: "wallet-select/restore" });
},
restoreViewWallet() {
this.$router.replace({ path: "wallet-select/import-view-only" });
},
importWallet() {
this.$router.replace({ path: "wallet-select/import" });
},
importOldGuiWallets() {
this.$router.replace({ path: "wallet-select/import-old-gui" });
},
importLegacyWallet() {
this.$router.replace({ path: "wallet-select/import-legacy" });
},
copyAddress(address, event) {
event.stopPropagation();
for (let i = 0; i < event.path.length; i++) {
if (event.path[i].tagName == "BUTTON") {
event.path[i].blur();
break;
}
}
clipboard.writeText(address);
this.$q.notify({
type: "positive",
timeout: 1000,
message: this.$t("notification.positive.addressCopied")
});
}
}
};
</script>
<style lang="scss">
.header-popover.q-popover {
max-width: 250px !important;
max-width: 250px !important;
}
.wallet-list {
.header {
margin: 0 16px;
margin-bottom: 8px;
min-height: 36px;
.header {
margin: 0 16px;
margin-bottom: 8px;
min-height: 36px;
.header-title {
font-size: 14px;
font-weight: 500;
}
.header-title {
font-size: 14px;
font-weight: 500;
}
.add {
width: 38px;
padding: 0;
}
}
.q-item {
margin: 10px 16px;
margin-bottom: 0px;
padding: 14px;
border-radius: 3px;
.add {
width: 38px;
padding: 0;
}
}
.q-item {
margin: 10px 16px;
margin-bottom: 0px;
padding: 14px;
border-radius: 3px;
}
}
</style>

View File

@ -1,219 +1,240 @@
<template>
<q-page>
<q-page>
<div class="q-mx-md">
<LokiField class="q-mt-md" :label="$t('fieldLabels.walletName')" :error="$v.wallet.name.$error">
<q-input
v-model="wallet.name"
:placeholder="$t('placeholders.walletName')"
@keyup.enter="restore_wallet"
@blur="$v.wallet.name.$touch"
:dark="theme=='dark'"
hide-underline
/>
</LokiField>
<LokiField class="q-mt-md" :label="$t('fieldLabels.walletName')" :error="$v.wallet.name.$error">
<q-input
v-model="wallet.name"
:placeholder="$t('placeholders.walletName')"
:dark="theme == 'dark'"
hide-underline
@keyup.enter="restore_wallet"
@blur="$v.wallet.name.$touch"
/>
</LokiField>
<LokiField class="q-mt-md" :label="$t('fieldLabels.mnemonicSeed')" :error="$v.wallet.seed.$error">
<q-input
v-model="wallet.seed"
:placeholder="$t('placeholders.mnemonicSeed')"
type="textarea"
@blur="$v.wallet.seed.$touch"
:dark="theme=='dark'"
hide-underline
/>
</LokiField>
<LokiField class="q-mt-md" :label="$t('fieldLabels.mnemonicSeed')" :error="$v.wallet.seed.$error">
<q-input
v-model="wallet.seed"
:placeholder="$t('placeholders.mnemonicSeed')"
type="textarea"
:dark="theme == 'dark'"
hide-underline
@blur="$v.wallet.seed.$touch"
/>
</LokiField>
<div class="row items-end q-mt-md">
<div class="col">
<LokiField v-if="wallet.refresh_type=='date'" :label="$t('fieldLabels.restoreFromDate')">
<q-datetime v-model="wallet.refresh_start_date" type="date"
modal :min="1525305600000" :max="Date.now()"
:dark="theme=='dark'"
hide-underline
/>
</LokiField>
<LokiField v-else-if="wallet.refresh_type=='height'" :label="$t('fieldLabels.restoreFromBlockHeight')" :error="$v.wallet.refresh_start_height.$error">
<q-input v-model="wallet.refresh_start_height" type="number"
min="0"
@blur="$v.wallet.refresh_start_height.$touch"
:dark="theme=='dark'"
hide-underline
/>
</LokiField>
</div>
<div class="col-auto q-ml-sm">
<template v-if="wallet.refresh_type=='date'">
<q-btn @click="wallet.refresh_type='height'" class="float-right" :text-color="theme=='dark'?'white':'dark'" flat>
<div style="min-width: 80px; height: 38px;" class="text-center flex column items-center justify-center">
<q-icon class="block" name="clear_all" />
<div style="font-size:10px">
{{ $t("strings.switchToHeightSelect") }}
</div>
</div>
</q-btn>
</template>
<template v-else-if="wallet.refresh_type=='height'">
<q-btn @click="wallet.refresh_type='date'" class="float-right" :text-color="theme=='dark'?'white':'dark'" flat>
<div style="min-width: 80px; height: 38px;" class="text-center flex column items-center justify-center">
<q-icon class="block" name="today" />
<div style="font-size:10px">
{{ $t("strings.switchToDateSelect") }}
</div>
</div>
</q-btn>
</template>
</div>
<div class="row items-end q-mt-md">
<div class="col">
<LokiField v-if="wallet.refresh_type == 'date'" :label="$t('fieldLabels.restoreFromDate')">
<q-datetime
v-model="wallet.refresh_start_date"
type="date"
modal
:min="1525305600000"
:max="Date.now()"
:dark="theme == 'dark'"
hide-underline
/>
</LokiField>
<LokiField
v-else-if="wallet.refresh_type == 'height'"
:label="$t('fieldLabels.restoreFromBlockHeight')"
:error="$v.wallet.refresh_start_height.$error"
>
<q-input
v-model="wallet.refresh_start_height"
type="number"
min="0"
:dark="theme == 'dark'"
hide-underline
@blur="$v.wallet.refresh_start_height.$touch"
/>
</LokiField>
</div>
<div class="col-auto q-ml-sm">
<template v-if="wallet.refresh_type == 'date'">
<q-btn
class="float-right"
:text-color="theme == 'dark' ? 'white' : 'dark'"
flat
@click="wallet.refresh_type = 'height'"
>
<div style="min-width: 80px; height: 38px;" class="text-center flex column items-center justify-center">
<q-icon class="block" name="clear_all" />
<div style="font-size:10px">
{{ $t("strings.switchToHeightSelect") }}
</div>
</div>
</q-btn>
</template>
<template v-else-if="wallet.refresh_type == 'height'">
<q-btn
class="float-right"
:text-color="theme == 'dark' ? 'white' : 'dark'"
flat
@click="wallet.refresh_type = 'date'"
>
<div style="min-width: 80px; height: 38px;" class="text-center flex column items-center justify-center">
<q-icon class="block" name="today" />
<div style="font-size:10px">
{{ $t("strings.switchToDateSelect") }}
</div>
</div>
</q-btn>
</template>
</div>
</div>
<LokiField class="q-mt-md" :label="$t('fieldLabels.password')">
<q-input
v-model="wallet.password"
:placeholder="$t('placeholders.walletPassword')"
@keyup.enter="restore_wallet"
type="password"
:dark="theme=='dark'"
hide-underline
/>
</LokiField>
<LokiField class="q-mt-md" :label="$t('fieldLabels.password')">
<q-input
v-model="wallet.password"
:placeholder="$t('placeholders.walletPassword')"
type="password"
:dark="theme == 'dark'"
hide-underline
@keyup.enter="restore_wallet"
/>
</LokiField>
<LokiField class="q-mt-md" :label="$t('fieldLabels.confirmPassword')">
<q-input
v-model="wallet.password_confirm"
@keyup.enter="restore_wallet"
type="password"
:dark="theme=='dark'"
hide-underline
/>
</LokiField>
<q-field>
<q-btn color="primary" @click="restore_wallet" :label="$t('buttons.restoreWallet')" />
</q-field>
<LokiField class="q-mt-md" :label="$t('fieldLabels.confirmPassword')">
<q-input
v-model="wallet.password_confirm"
type="password"
:dark="theme == 'dark'"
hide-underline
@keyup.enter="restore_wallet"
/>
</LokiField>
<q-field>
<q-btn color="primary" :label="$t('buttons.restoreWallet')" @click="restore_wallet" />
</q-field>
</div>
</q-page>
</q-page>
</template>
<script>
import { required, numeric } from "vuelidate/lib/validators"
import { mapState } from "vuex"
import LokiField from "components/loki_field"
import { required, numeric } from "vuelidate/lib/validators";
import { mapState } from "vuex";
import LokiField from "components/loki_field";
export default {
data () {
return {
wallet: {
name: "",
seed: "",
refresh_type: "date",
refresh_start_height: 0,
refresh_start_date: 1525305600000, // timestamp of block 1
password: "",
password_confirm: ""
},
components: {
LokiField
},
data() {
return {
wallet: {
name: "",
seed: "",
refresh_type: "date",
refresh_start_height: 0,
refresh_start_date: 1525305600000, // timestamp of block 1
password: "",
password_confirm: ""
}
};
},
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
status: state => state.gateway.wallet.status
}),
watch: {
status: {
handler(val, old) {
if (val.code == old.code) return;
switch (this.status.code) {
case 1:
break;
case 0:
this.$q.loading.hide();
this.$router.replace({
path: "/wallet-select/created"
});
break;
default:
this.$q.loading.hide();
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.status.message
});
break;
}
},
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
status: state => state.gateway.wallet.status,
}),
watch: {
status: {
handler(val, old){
if(val.code == old.code) return
switch(this.status.code) {
case 1:
break;
case 0:
this.$q.loading.hide()
this.$router.replace({ path: "/wallet-select/created" });
break;
default:
this.$q.loading.hide()
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.status.message
})
break;
}
},
deep: true
}
},
validations: {
wallet: {
name: { required },
seed: { required },
refresh_start_height: { numeric }
}
},
methods: {
restore_wallet() {
this.$v.wallet.$touch()
if (this.$v.wallet.name.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.enterWalletName")
})
return
}
if (this.$v.wallet.seed.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.enterSeedWords")
})
return
}
let seed = this.wallet.seed.trim()
.replace(/\n/g, " ")
.replace(/\t/g, " ")
.replace(/\s{2,}/g, " ")
.split(" ")
if(seed.length !== 14 && seed.length !== 24 && seed.length !== 25 && seed.length !== 26) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.invalidSeedLength")
})
return
}
if (this.$v.wallet.refresh_start_height.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.invalidRestoreHeight")
})
return
}
if(this.wallet.password != this.wallet.password_confirm) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.passwordNoMatch")
})
return
}
this.$q.loading.show({
delay: 0
})
this.$gateway.send("wallet", "restore_wallet", this.wallet);
},
cancel() {
this.$router.replace({ path: "/wallet-select" });
}
},
components: {
LokiField
},
deep: true
}
}
},
validations: {
wallet: {
name: { required },
seed: { required },
refresh_start_height: { numeric }
}
},
methods: {
restore_wallet() {
this.$v.wallet.$touch();
if (this.$v.wallet.name.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.enterWalletName")
});
return;
}
if (this.$v.wallet.seed.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.enterSeedWords")
});
return;
}
let seed = this.wallet.seed
.trim()
.replace(/\n/g, " ")
.replace(/\t/g, " ")
.replace(/\s{2,}/g, " ")
.split(" ");
if (seed.length !== 14 && seed.length !== 24 && seed.length !== 25 && seed.length !== 26) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.invalidSeedLength")
});
return;
}
if (this.$v.wallet.refresh_start_height.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.invalidRestoreHeight")
});
return;
}
if (this.wallet.password != this.wallet.password_confirm) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.passwordNoMatch")
});
return;
}
this.$q.loading.show({
delay: 0
});
this.$gateway.send("wallet", "restore_wallet", this.wallet);
},
cancel() {
this.$router.replace({ path: "/wallet-select" });
}
}
};
</script>
<style>
</style>
<style></style>

View File

@ -1,178 +1,176 @@
<template>
<q-page class="address-book">
<q-page class="address-book">
<div class="header row q-pt-md q-pb-xs q-mx-md q-mb-none items-center non-selectable">
{{ $t("titles.addressBook") }}
{{ $t("titles.addressBook") }}
</div>
<template v-if="address_book_combined.length">
<q-list link no-border :dark="theme=='dark'" class="loki-list">
<q-item class="loki-list-item" v-for="(entry, index) in address_book_combined" @click.native="details(entry)" :key="`${entry.address}-${entry.name}`">
<q-item-main>
<q-item-tile class="ellipsis" label>{{ entry.address }}</q-item-tile>
<q-item-tile sublabel class="non-selectable">{{ entry.name }}</q-item-tile>
</q-item-main>
<q-item-side>
<q-icon size="24px" :name="entry.starred ? 'star' : 'star_border'" />
<q-btn
color="secondary"
style="margin-left: 10px;"
:label="$t('buttons.send')"
:disabled="view_only"
@click="sendToAddress(entry, $event)"
/>
</q-item-side>
<q-list link no-border :dark="theme == 'dark'" class="loki-list">
<q-item
v-for="entry in address_book_combined"
:key="`${entry.address}-${entry.name}`"
class="loki-list-item"
@click.native="details(entry)"
>
<q-item-main>
<q-item-tile class="ellipsis" label>{{ entry.address }}</q-item-tile>
<q-item-tile sublabel class="non-selectable">{{ entry.name }}</q-item-tile>
</q-item-main>
<q-item-side>
<q-icon size="24px" :name="entry.starred ? 'star' : 'star_border'" />
<q-btn
color="secondary"
style="margin-left: 10px;"
:label="$t('buttons.send')"
:disabled="view_only"
@click="sendToAddress(entry, $event)"
/>
</q-item-side>
<q-context-menu>
<q-list link separator style="min-width: 150px; max-height: 300px;">
<q-item v-close-overlay
@click.native="details(entry)">
<q-item-main :label="$t('menuItems.showDetails')" />
</q-item>
<q-context-menu>
<q-list link separator style="min-width: 150px; max-height: 300px;">
<q-item v-close-overlay @click.native="details(entry)">
<q-item-main :label="$t('menuItems.showDetails')" />
</q-item>
<q-item v-close-overlay
@click.native="sendToAddress(entry, $event)">
<q-item-main :label="$t('menuItems.sendToThisAddress')" />
</q-item>
<q-item v-close-overlay @click.native="sendToAddress(entry, $event)">
<q-item-main :label="$t('menuItems.sendToThisAddress')" />
</q-item>
<q-item v-close-overlay
@click.native="copyAddress(entry, $event)">
<q-item-main :label="$t('menuItems.copyAddress')" />
</q-item>
</q-list>
</q-context-menu>
</q-item>
</q-list>
<q-item v-close-overlay @click.native="copyAddress(entry, $event)">
<q-item-main :label="$t('menuItems.copyAddress')" />
</q-item>
</q-list>
</q-context-menu>
</q-item>
</q-list>
</template>
<template v-else>
<p class="q-ma-md">{{ $t("strings.addressBookIsEmpty") }}</p>
<p class="q-ma-md">{{ $t("strings.addressBookIsEmpty") }}</p>
</template>
<q-page-sticky position="bottom-right" :offset="[18, 18]">
<q-btn
:disable="!is_ready"
round
color="primary"
@click="addEntry"
icon="add"
/>
<q-btn :disable="!is_ready" round color="primary" icon="add" @click="addEntry" />
</q-page-sticky>
<AddressBookDetails ref="addressBookDetails" />
</q-page>
</q-page>
</template>
<script>
const { clipboard } = require("electron")
import { mapState } from "vuex"
import Identicon from "components/identicon"
import AddressBookDetails from "components/address_book_details"
const { clipboard } = require("electron");
import { mapState } from "vuex";
import AddressBookDetails from "components/address_book_details";
export default {
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
view_only: state => state.gateway.wallet.info.view_only,
address_book: state => state.gateway.wallet.address_list.address_book,
address_book_starred: state => state.gateway.wallet.address_list.address_book_starred,
is_ready (state) {
return this.$store.getters["gateway/isReady"]
},
address_book_combined (state) {
const starred = this.address_book_starred.map(a => ({ ...a, starred: true }));
return [
...starred,
...this.address_book
]
}
}),
methods: {
details: function (entry) {
this.$refs.addressBookDetails.entry = entry
this.$refs.addressBookDetails.mode = "view"
this.$refs.addressBookDetails.isVisible = true
},
addEntry: function () {
this.$refs.addressBookDetails.entry = null
this.$refs.addressBookDetails.mode = "new"
this.$refs.addressBookDetails.isVisible = true
},
sendToAddress (address, event) {
event.stopPropagation()
for(let i = 0; i < event.path.length; i++) {
if(event.path[i].tagName == "BUTTON") {
event.path[i].blur()
break
}
}
this.$router.replace({ path: "send", query: {address: address.address, payment_id: address.payment_id} });
},
copyAddress (entry, event) {
event.stopPropagation()
for(let i = 0; i < event.path.length; i++) {
if(event.path[i].tagName == "BUTTON") {
event.path[i].blur()
break
}
}
clipboard.writeText(entry.address)
if(entry.payment_id) {
this.$q.dialog({
title: this.$t("dialog.copyAddress.title"),
message: this.$t("dialog.copyAddress.message"),
ok: {
label: this.$t("dialog.buttons.ok")
},
}).catch(() => null).then(password => {
this.$q.notify({
type: "positive",
timeout: 1000,
message: this.$t("notification.positive.addressCopied")
})
})
} else {
this.$q.notify({
type: "positive",
timeout: 1000,
message: this.$t("notification.positive.addressCopied")
})
}
}
components: {
AddressBookDetails
},
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
view_only: state => state.gateway.wallet.info.view_only,
address_book: state => state.gateway.wallet.address_list.address_book,
address_book_starred: state => state.gateway.wallet.address_list.address_book_starred,
is_ready() {
return this.$store.getters["gateway/isReady"];
},
components: {
Identicon,
AddressBookDetails
address_book_combined() {
const starred = this.address_book_starred.map(a => ({
...a,
starred: true
}));
return [...starred, ...this.address_book];
}
}
}),
methods: {
details: function(entry) {
this.$refs.addressBookDetails.entry = entry;
this.$refs.addressBookDetails.mode = "view";
this.$refs.addressBookDetails.isVisible = true;
},
addEntry: function() {
this.$refs.addressBookDetails.entry = null;
this.$refs.addressBookDetails.mode = "new";
this.$refs.addressBookDetails.isVisible = true;
},
sendToAddress(address, event) {
event.stopPropagation();
for (let i = 0; i < event.path.length; i++) {
if (event.path[i].tagName == "BUTTON") {
event.path[i].blur();
break;
}
}
this.$router.replace({
path: "send",
query: {
address: address.address,
payment_id: address.payment_id
}
});
},
copyAddress(entry, event) {
event.stopPropagation();
for (let i = 0; i < event.path.length; i++) {
if (event.path[i].tagName == "BUTTON") {
event.path[i].blur();
break;
}
}
clipboard.writeText(entry.address);
if (entry.payment_id) {
this.$q
.dialog({
title: this.$t("dialog.copyAddress.title"),
message: this.$t("dialog.copyAddress.message"),
ok: {
label: this.$t("dialog.buttons.ok")
}
})
.catch(() => null)
.then(() => {
this.$q.notify({
type: "positive",
timeout: 1000,
message: this.$t("notification.positive.addressCopied")
});
});
} else {
this.$q.notify({
type: "positive",
timeout: 1000,
message: this.$t("notification.positive.addressCopied")
});
}
}
}
};
</script>
<style lang="scss">
.address-book {
.header {
font-size: 14px;
font-weight: 500
.header {
font-size: 14px;
font-weight: 500;
}
.loki-list-item {
cursor: pointer;
padding-top: 12px;
padding-bottom: 12px;
.q-item-sublabel {
font-size: 14px;
}
.loki-list-item {
cursor: pointer;
padding-top: 12px;
padding-bottom: 12px;
.q-item-sublabel, {
font-size: 14px;
}
.q-item-label {
font-weight: 400;
}
.q-item-side {
display: flex;
justify-content: center;
align-items: center;
margin-left: 12px;
}
.q-item-label {
font-weight: 400;
}
.q-item-side {
display: flex;
justify-content: center;
align-items: center;
margin-left: 12px;
}
}
}
</style>

View File

@ -1,36 +1,38 @@
<template>
<q-page class="advanced-page">
<q-page class="advanced-page">
<div class="header row items-center justify-center q-pt-md">
<q-btn-toggle
v-model="screen"
toggle-color="primary"
color="secondary"
:options="[
{label: $t('titles.advanced.prove'), value: 'prove'},
{label: $t('titles.advanced.checkTransaction'), value: 'check'}
]"
/>
<q-btn-toggle
v-model="screen"
toggle-color="primary"
color="secondary"
:options="[
{ label: $t('titles.advanced.prove'), value: 'prove' },
{
label: $t('titles.advanced.checkTransaction'),
value: 'check'
}
]"
/>
</div>
<ProveTransaction v-if="screen === 'prove'"/>
<CheckTransaction v-if="screen === 'check'"/>
</q-page>
<ProveTransaction v-if="screen === 'prove'" />
<CheckTransaction v-if="screen === 'check'" />
</q-page>
</template>
<script>
import ProveTransaction from "components/prove_transaction"
import CheckTransaction from "components/check_transaction"
import ProveTransaction from "components/prove_transaction";
import CheckTransaction from "components/check_transaction";
export default {
data () {
return {
screen: "prove",
}
},
components: {
ProveTransaction,
CheckTransaction
}
}
components: {
ProveTransaction,
CheckTransaction
},
data() {
return {
screen: "prove"
};
}
};
</script>
<style lang="scss">
</style>
<style lang="scss"></style>

View File

@ -1,14 +1,11 @@
<template>
<q-page class="flex flex-center">
</q-page>
<q-page class="flex flex-center"> </q-page>
</template>
<style>
</style>
<style></style>
<script>
export default {
name: "PageIndex"
}
name: "PageIndex"
};
</script>

View File

@ -1,195 +1,195 @@
<template>
<q-page class="receive">
<q-list link no-border :dark="theme=='dark'" class="loki-list">
<q-page class="receive">
<q-list link no-border :dark="theme == 'dark'" class="loki-list">
<q-list-header>{{ $t("strings.addresses.myPrimaryAddress") }}</q-list-header>
<ReceiveItem
v-for="address in address_list.primary"
:key="address.address"
class="primary-address"
:address="address"
:sublabel="$t('strings.addresses.primaryAddress')"
:show-q-r="showQR"
:copy-address="copyAddress"
:details="details"
white-q-r-icon
/>
<q-list-header>{{ $t("strings.addresses.myPrimaryAddress") }}</q-list-header>
<receive-item
class="primary-address"
v-for="address in address_list.primary"
:key="address.address"
:address="address"
:sublabel="$t('strings.addresses.primaryAddress')"
:showQR="showQR"
:copyAddress="copyAddress"
:details="details"
whiteQRIcon
<template v-if="address_list.used.length">
<q-list-header>{{ $t("strings.addresses.myUsedAddresses") }}</q-list-header>
<ReceiveItem
v-for="address in address_list.used"
:key="address.address"
:address="address"
:sublabel="
`${$t('strings.addresses.subAddress')} (${$t('strings.addresses.subAddressIndex', {
index: address.address_index
})})`
"
:show-q-r="showQR"
:copy-address="copyAddress"
:details="details"
/>
</template>
<template v-if="address_list.used.length">
<q-list-header>{{ $t("strings.addresses.myUsedAddresses") }}</q-list-header>
<receive-item
v-for="address in address_list.used"
:key="address.address"
:address="address"
:sublabel="`${$t('strings.addresses.subAddress')} (${$t('strings.addresses.subAddressIndex', { index: address.address_index })})`"
:showQR="showQR"
:copyAddress="copyAddress"
:details="details"
/>
</template>
<template v-if="address_list.unused.length">
<q-list-header>{{ $t("strings.addresses.myUnusedAddresses") }}</q-list-header>
<receive-item
v-for="address in address_list.unused"
:key="address.address"
:address="address"
:sublabel="`${$t('strings.addresses.subAddress')} (${$t('strings.addresses.subAddressIndex', { index: address.address_index })})`"
:showQR="showQR"
:copyAddress="copyAddress"
:details="details"
:shouldShowInfo="false"
/>
</template>
<template v-if="address_list.unused.length">
<q-list-header>{{ $t("strings.addresses.myUnusedAddresses") }}</q-list-header>
<ReceiveItem
v-for="address in address_list.unused"
:key="address.address"
:address="address"
:sublabel="
`${$t('strings.addresses.subAddress')} (${$t('strings.addresses.subAddressIndex', {
index: address.address_index
})})`
"
:show-q-r="showQR"
:copy-address="copyAddress"
:details="details"
:should-show-info="false"
/>
</template>
</q-list>
<AddressDetails ref="addressDetails" />
<!-- QR Code -->
<template v-if="QR.address != null">
<q-modal v-model="QR.visible" minimized :content-css="{padding: '25px'}">
<q-modal v-model="QR.visible" minimized :content-css="{ padding: '25px' }">
<div class="text-center q-mb-sm q-pa-md" style="background: white;">
<QrcodeVue ref="qr" :value="QR.address" size="240"> </QrcodeVue>
<q-context-menu>
<q-list link separator style="min-width: 150px; max-height: 300px;">
<q-item v-close-overlay @click.native="copyQR()">
<q-item-main :label="$t('menuItems.copyQR')" />
</q-item>
<q-item v-close-overlay @click.native="saveQR()">
<q-item-main :label="$t('menuItems.saveQR')" />
</q-item>
</q-list>
</q-context-menu>
</div>
<div class="text-center q-mb-sm q-pa-md" style="background: white;">
<qrcode-vue :value="QR.address" size="240" ref="qr">
</qrcode-vue>
<q-context-menu>
<q-list link separator style="min-width: 150px; max-height: 300px;">
<q-item v-close-overlay @click.native="copyQR()">
<q-item-main :label="$t('menuItems.copyQR')" />
</q-item>
<q-item v-close-overlay @click.native="saveQR()">
<q-item-main :label="$t('menuItems.saveQR')" />
</q-item>
</q-list>
</q-context-menu>
</div>
<q-btn
color="primary"
@click="QR.visible = false"
:label="$t('buttons.close')"
/>
</q-modal>
<q-btn color="primary" :label="$t('buttons.close')" @click="QR.visible = false" />
</q-modal>
</template>
</q-page>
</q-page>
</template>
<script>
const { clipboard, nativeImage } = require("electron")
import { mapState } from "vuex"
const { clipboard, nativeImage } = require("electron");
import { mapState } from "vuex";
import QrcodeVue from "qrcode.vue";
import AddressDetails from "components/address_details"
import ReceiveItem from "components/receive_item"
import AddressDetails from "components/address_details";
import ReceiveItem from "components/receive_item";
export default {
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
address_list: state => state.gateway.wallet.address_list
}),
filters: {
toString: function (value) {
if (typeof value !== "number") return "N/A";
return String(value);
},
currency: function (value) {
if (typeof value !== "number") return "N/A";
filters: {
toString: function(value) {
if (typeof value !== "number") return "N/A";
return String(value);
},
currency: function(value) {
if (typeof value !== "number") return "N/A";
const amount = value / 1e9
return amount.toLocaleString()
}
},
data () {
return {
QR: {
visible: false,
address: null,
}
}
},
methods: {
details (address) {
console.log(address)
this.$refs.addressDetails.address = address;
this.$refs.addressDetails.isVisible = true;
},
showQR (address, event) {
event.stopPropagation()
this.QR.visible = true
this.QR.address = address
},
copyQR () {
const data = this.$refs.qr.$el.childNodes[0].toDataURL()
const img = nativeImage.createFromDataURL(data)
clipboard.writeImage(img)
this.$q.notify({
type: "positive",
timeout: 1000,
message: this.$t("notification.positive.qrCopied")
})
},
saveQR () {
let img = this.$refs.qr.$el.childNodes[0].toDataURL()
this.$gateway.send("core", "save_png", {img, type: "QR Code"})
},
copyAddress (address, event) {
event.stopPropagation()
for(let i = 0; i < event.path.length; i++) {
if(event.path[i].tagName == "BUTTON") {
event.path[i].blur()
break
}
}
clipboard.writeText(address)
this.$q.notify({
type: "positive",
timeout: 1000,
message: this.$t("notification.positive.addressCopied")
})
}
},
components: {
AddressDetails,
QrcodeVue,
ReceiveItem
const amount = value / 1e9;
return amount.toLocaleString();
}
}
},
components: {
AddressDetails,
QrcodeVue,
ReceiveItem
},
data() {
return {
QR: {
visible: false,
address: null
}
};
},
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
address_list: state => state.gateway.wallet.address_list
}),
methods: {
details(address) {
console.log(address);
this.$refs.addressDetails.address = address;
this.$refs.addressDetails.isVisible = true;
},
showQR(address, event) {
event.stopPropagation();
this.QR.visible = true;
this.QR.address = address;
},
copyQR() {
const data = this.$refs.qr.$el.childNodes[0].toDataURL();
const img = nativeImage.createFromDataURL(data);
clipboard.writeImage(img);
this.$q.notify({
type: "positive",
timeout: 1000,
message: this.$t("notification.positive.qrCopied")
});
},
saveQR() {
let img = this.$refs.qr.$el.childNodes[0].toDataURL();
this.$gateway.send("core", "save_png", { img, type: "QR Code" });
},
copyAddress(address, event) {
event.stopPropagation();
for (let i = 0; i < event.path.length; i++) {
if (event.path[i].tagName == "BUTTON") {
event.path[i].blur();
break;
}
}
clipboard.writeText(address);
this.$q.notify({
type: "positive",
timeout: 1000,
message: this.$t("notification.positive.addressCopied")
});
}
}
};
</script>
<style lang="scss">
.receive {
.q-item-label {
font-weight: 400;
.q-item-label {
font-weight: 400;
}
.q-item-sublabel,
.q-list-header {
font-size: 13px;
}
.loki-list-item {
cursor: pointer;
.q-item {
padding-top: 4px;
padding-bottom: 4px;
}
.q-item-sublabel, .q-list-header {
font-size: 13px;
.q-item-side {
display: flex;
justify-content: center;
align-items: center;
}
.loki-list-item {
cursor: pointer;
.info {
span {
font-size: 14px;
}
.q-item {
padding-top: 4px;
padding-bottom: 4px;
}
.q-item-side {
display: flex;
justify-content: center;
align-items: center;
}
.info {
span {
font-size: 14px;
}
.value {
font-size: 16px;
font-weight: bold;
}
}
.value {
font-size: 16px;
font-weight: bold;
}
}
}
}
</style>

View File

@ -1,331 +1,346 @@
<template>
<q-page class="send">
<q-page class="send">
<template v-if="view_only">
<div class="q-pa-md">
{{ $t("strings.viewOnlyMode") }}
</div>
<div class="q-pa-md">
{{ $t("strings.viewOnlyMode") }}
</div>
</template>
<template v-else>
<div class="q-pa-md">
<div class="row gutter-md">
<!-- Amount -->
<div class="col-6">
<LokiField :label="$t('fieldLabels.amount')" :error="$v.newTx.amount.$error">
<q-input v-model="newTx.amount"
:dark="theme=='dark'"
type="number"
min="0"
:max="unlocked_balance / 1e9"
placeholder="0"
@blur="$v.newTx.amount.$touch"
hide-underline
/>
<q-btn color="secondary" @click="newTx.amount = unlocked_balance / 1e9" :text-color="theme=='dark'?'white':'dark'">
{{ $t("buttons.all") }}
</q-btn>
</LokiField>
</div>
<!-- Priority -->
<div class="col-6">
<LokiField :label="$t('fieldLabels.priority')">
<q-select :dark="theme=='dark'"
v-model="newTx.priority"
:options="priorityOptions"
hide-underline
/>
</LokiField>
</div>
</div>
<!-- Address -->
<div class="col q-mt-sm">
<LokiField :label="$t('fieldLabels.address')" :error="$v.newTx.address.$error">
<q-input v-model.trim="newTx.address"
:dark="theme=='dark'"
@blur="$v.newTx.address.$touch"
:placeholder="address_placeholder"
hide-underline
/>
<q-btn color="secondary" :text-color="theme=='dark'?'white':'dark'" to="addressbook">
{{ $t("buttons.contacts") }}
</q-btn>
</LokiField>
</div>
<!-- Payment ID -->
<div class="col q-mt-sm">
<LokiField :label="$t('fieldLabels.paymentId')" :error="$v.newTx.payment_id.$error" optional>
<q-input v-model.trim="newTx.payment_id"
:dark="theme=='dark'"
@blur="$v.newTx.payment_id.$touch"
:placeholder="$t('placeholders.hexCharacters', { count: '16 or 64' })"
hide-underline
/>
</LokiField>
</div>
<!-- Notes -->
<div class="col q-mt-sm">
<LokiField :label="$t('fieldLabels.notes')" optional>
<q-input v-model="newTx.note"
type="textarea"
:dark="theme=='dark'"
:placeholder="$t('placeholders.transactionNotes')"
hide-underline
/>
</LokiField>
</div>
<!-- Save to address book -->
<q-field>
<q-checkbox v-model="newTx.address_book.save" :label="$t('strings.saveToAddressBook')" :dark="theme=='dark'" />
</q-field>
<div v-if="newTx.address_book.save">
<LokiField :label="$t('fieldLabels.name')" optional>
<q-input v-model="newTx.address_book.name"
:dark="theme=='dark'"
:placeholder="$t('placeholders.addressBookName')"
hide-underline
/>
</LokiField>
<LokiField class="q-mt-sm" :label="$t('fieldLabels.notes')" optional>
<q-input v-model="newTx.address_book.description"
type="textarea"
rows="2"
:dark="theme=='dark'"
:placeholder="$t('placeholders.additionalNotes')"
hide-underline
/>
</LokiField>
</div>
<q-field class="q-pt-sm">
<q-btn
class="send-btn"
:disable="!is_able_to_send"
color="primary" @click="send()" :label="$t('buttons.send')" />
</q-field>
<div class="q-pa-md">
<div class="row gutter-md">
<!-- Amount -->
<div class="col-6">
<LokiField :label="$t('fieldLabels.amount')" :error="$v.newTx.amount.$error">
<q-input
v-model="newTx.amount"
:dark="theme == 'dark'"
type="number"
min="0"
:max="unlocked_balance / 1e9"
placeholder="0"
hide-underline
@blur="$v.newTx.amount.$touch"
/>
<q-btn
color="secondary"
:text-color="theme == 'dark' ? 'white' : 'dark'"
@click="newTx.amount = unlocked_balance / 1e9"
>
{{ $t("buttons.all") }}
</q-btn>
</LokiField>
</div>
<!-- Priority -->
<div class="col-6">
<LokiField :label="$t('fieldLabels.priority')">
<q-select v-model="newTx.priority" :dark="theme == 'dark'" :options="priorityOptions" hide-underline />
</LokiField>
</div>
</div>
<q-inner-loading :visible="tx_status.sending" :dark="theme=='dark'">
<q-spinner color="primary" :size="30" />
</q-inner-loading>
<!-- Address -->
<div class="col q-mt-sm">
<LokiField :label="$t('fieldLabels.address')" :error="$v.newTx.address.$error">
<q-input
v-model.trim="newTx.address"
:dark="theme == 'dark'"
:placeholder="address_placeholder"
hide-underline
@blur="$v.newTx.address.$touch"
/>
<q-btn color="secondary" :text-color="theme == 'dark' ? 'white' : 'dark'" to="addressbook">
{{ $t("buttons.contacts") }}
</q-btn>
</LokiField>
</div>
<!-- Payment ID -->
<div class="col q-mt-sm">
<LokiField :label="$t('fieldLabels.paymentId')" :error="$v.newTx.payment_id.$error" optional>
<q-input
v-model.trim="newTx.payment_id"
:dark="theme == 'dark'"
:placeholder="
$t('placeholders.hexCharacters', {
count: '16 or 64'
})
"
hide-underline
@blur="$v.newTx.payment_id.$touch"
/>
</LokiField>
</div>
<!-- Notes -->
<div class="col q-mt-sm">
<LokiField :label="$t('fieldLabels.notes')" optional>
<q-input
v-model="newTx.note"
type="textarea"
:dark="theme == 'dark'"
:placeholder="$t('placeholders.transactionNotes')"
hide-underline
/>
</LokiField>
</div>
<!-- Save to address book -->
<q-field>
<q-checkbox
v-model="newTx.address_book.save"
:label="$t('strings.saveToAddressBook')"
:dark="theme == 'dark'"
/>
</q-field>
<div v-if="newTx.address_book.save">
<LokiField :label="$t('fieldLabels.name')" optional>
<q-input
v-model="newTx.address_book.name"
:dark="theme == 'dark'"
:placeholder="$t('placeholders.addressBookName')"
hide-underline
/>
</LokiField>
<LokiField class="q-mt-sm" :label="$t('fieldLabels.notes')" optional>
<q-input
v-model="newTx.address_book.description"
type="textarea"
rows="2"
:dark="theme == 'dark'"
:placeholder="$t('placeholders.additionalNotes')"
hide-underline
/>
</LokiField>
</div>
<q-field class="q-pt-sm">
<q-btn
class="send-btn"
:disable="!is_able_to_send"
color="primary"
:label="$t('buttons.send')"
@click="send()"
/>
</q-field>
</div>
<q-inner-loading :visible="tx_status.sending" :dark="theme == 'dark'">
<q-spinner color="primary" :size="30" />
</q-inner-loading>
</template>
</q-page>
</q-page>
</template>
<script>
import { mapState } from "vuex"
import { required, decimal } from "vuelidate/lib/validators"
import { payment_id, address, greater_than_zero } from "src/validators/common"
import Identicon from "components/identicon"
import LokiField from "components/loki_field"
import WalletPassword from "src/mixins/wallet_password"
import { mapState } from "vuex";
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";
const objectAssignDeep = require("object-assign-deep");
export default {
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
view_only: state => state.gateway.wallet.info.view_only,
unlocked_balance: state => state.gateway.wallet.info.unlocked_balance,
tx_status: state => state.gateway.tx_status,
is_ready (state) {
return this.$store.getters["gateway/isReady"]
},
is_able_to_send (state) {
return this.$store.getters["gateway/isAbleToSend"]
},
address_placeholder (state) {
const wallet = state.gateway.wallet.info;
const prefix = (wallet && wallet.address && wallet.address[0]) || "L";
return `${prefix}..`;
}
}),
data () {
const blink = 0x626c6e6b
return {
sending: false,
newTx: {
amount: 0,
address: "",
payment_id: "",
priority: blink,
address_book: {
save: false,
name: "",
description: ""
}
},
priorityOptions: [
{ label: this.$t("strings.priorityOptions.blink"), value: blink }, // Blink
{ label: this.$t("strings.priorityOptions.slow"), value: 1 } // Slow
]
components: {
LokiField
},
mixins: [WalletPassword],
data() {
const blink = 0x626c6e6b;
return {
sending: false,
newTx: {
amount: 0,
address: "",
payment_id: "",
priority: blink,
address_book: {
save: false,
name: "",
description: ""
}
},
priorityOptions: [
{
label: this.$t("strings.priorityOptions.blink"),
value: blink
}, // Blink
{ label: this.$t("strings.priorityOptions.slow"), value: 1 } // Slow
]
};
},
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
view_only: state => state.gateway.wallet.info.view_only,
unlocked_balance: state => state.gateway.wallet.info.unlocked_balance,
tx_status: state => state.gateway.tx_status,
is_ready() {
return this.$store.getters["gateway/isReady"];
},
validations: {
newTx: {
amount: {
required,
decimal,
greater_than_zero
},
address: {
required,
isAddress(value) {
if (value === '') return true
return new Promise(resolve => {
address(value, this.$gateway)
.then(() => resolve(true))
.catch(e => resolve(false))
});
}
},
payment_id: { payment_id }
}
is_able_to_send() {
return this.$store.getters["gateway/isAbleToSend"];
},
watch: {
tx_status: {
handler(val, old){
if(val.code == old.code) return
switch(this.tx_status.code) {
case 0:
this.$q.notify({
type: "positive",
timeout: 1000,
message: this.tx_status.message
})
this.$v.$reset();
this.newTx = {
amount: 0,
address: "",
payment_id: "",
priority: 0,
address_book: {
save: false,
name: "",
description: ""
},
note: ""
}
break;
case -1:
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.tx_status.message
})
break;
}
},
deep: true
},
$route (to) {
if(to.path == "/wallet/send" && to.query.hasOwnProperty("address")) {
this.autoFill(to.query)
}
}
},
mounted () {
if(this.$route.path == "/wallet/send" && this.$route.query.hasOwnProperty("address")) {
this.autoFill(this.$route.query)
}
},
methods: {
autoFill: function (info) {
this.newTx.address = info.address
this.newTx.payment_id = info.payment_id
},
send: function () {
this.$v.newTx.$touch()
if(this.newTx.amount < 0) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.negativeAmount")
})
return
} else if(this.newTx.amount == 0) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.zeroAmount")
})
return
} else if(this.newTx.amount > this.unlocked_balance / 1e9) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.notEnoughBalance")
})
return
} else if (this.$v.newTx.amount.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.invalidAmount")
})
return
}
if (this.$v.newTx.address.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.invalidAddress")
})
return
}
if (this.$v.newTx.payment_id.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.invalidPaymentId")
})
return
}
this.showPasswordConfirmation({
title: this.$t("dialog.transfer.title"),
noPasswordMessage: this.$t("dialog.transfer.message"),
ok: {
label: this.$t("dialog.transfer.ok")
},
}).then(password => {
this.$store.commit("gateway/set_tx_status", {
code: 1,
message: "Sending transaction",
sending: true
})
const newTx = objectAssignDeep.noMutate(this.newTx, {password})
this.$gateway.send("wallet", "transfer", newTx)
}).catch(() => {
})
}
},
mixins: [WalletPassword],
components: {
Identicon,
LokiField
address_placeholder(state) {
const wallet = state.gateway.wallet.info;
const prefix = (wallet && wallet.address && wallet.address[0]) || "L";
return `${prefix}..`;
}
}
}),
validations: {
newTx: {
amount: {
required,
decimal,
greater_than_zero
},
address: {
required,
isAddress(value) {
if (value === "") return true;
return new Promise(resolve => {
address(value, this.$gateway)
.then(() => resolve(true))
.catch(() => resolve(false));
});
}
},
payment_id: { payment_id }
}
},
watch: {
tx_status: {
handler(val, old) {
if (val.code == old.code) return;
switch (this.tx_status.code) {
case 0:
this.$q.notify({
type: "positive",
timeout: 1000,
message: this.tx_status.message
});
this.$v.$reset();
this.newTx = {
amount: 0,
address: "",
payment_id: "",
priority: 0,
address_book: {
save: false,
name: "",
description: ""
},
note: ""
};
break;
case -1:
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.tx_status.message
});
break;
}
},
deep: true
},
$route(to) {
if (to.path == "/wallet/send" && to.query.hasOwnProperty("address")) {
this.autoFill(to.query);
}
}
},
mounted() {
if (this.$route.path == "/wallet/send" && this.$route.query.hasOwnProperty("address")) {
this.autoFill(this.$route.query);
}
},
methods: {
autoFill: function(info) {
this.newTx.address = info.address;
this.newTx.payment_id = info.payment_id;
},
send: function() {
this.$v.newTx.$touch();
if (this.newTx.amount < 0) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.negativeAmount")
});
return;
} else if (this.newTx.amount == 0) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.zeroAmount")
});
return;
} else if (this.newTx.amount > this.unlocked_balance / 1e9) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.notEnoughBalance")
});
return;
} else if (this.$v.newTx.amount.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.invalidAmount")
});
return;
}
if (this.$v.newTx.address.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.invalidAddress")
});
return;
}
if (this.$v.newTx.payment_id.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.invalidPaymentId")
});
return;
}
this.showPasswordConfirmation({
title: this.$t("dialog.transfer.title"),
noPasswordMessage: this.$t("dialog.transfer.message"),
ok: {
label: this.$t("dialog.transfer.ok")
}
})
.then(password => {
this.$store.commit("gateway/set_tx_status", {
code: 1,
message: "Sending transaction",
sending: true
});
const newTx = objectAssignDeep.noMutate(this.newTx, {
password
});
this.$gateway.send("wallet", "transfer", newTx);
})
.catch(() => {});
}
}
};
</script>
<style lang="scss">
.send {
.send-btn {
width: 200px;
}
.send-btn {
width: 200px;
}
}
</style>

View File

@ -1,36 +1,41 @@
<template>
<q-page class="service-node-page">
<q-page class="service-node-page">
<div class="header row items-center justify-center q-pt-md">
<q-btn-toggle
v-model="screen"
toggle-color="primary"
color="secondary"
:options="[
{label: $t('titles.serviceNode.staking'), value: 'staking'},
{label: $t('titles.serviceNode.registration'), value: 'registration'}
]"
/>
<q-btn-toggle
v-model="screen"
toggle-color="primary"
color="secondary"
:options="[
{
label: $t('titles.serviceNode.staking'),
value: 'staking'
},
{
label: $t('titles.serviceNode.registration'),
value: 'registration'
}
]"
/>
</div>
<ServiceNodeStaking v-if="screen === 'staking'"/>
<ServiceNodeStaking v-if="screen === 'staking'" />
<ServiceNodeRegistration v-if="screen === 'registration'" />
</q-page>
</q-page>
</template>
<script>
import ServiceNodeStaking from "components/service_node_staking"
import ServiceNodeRegistration from "components/service_node_registration"
import ServiceNodeStaking from "components/service_node_staking";
import ServiceNodeRegistration from "components/service_node_registration";
export default {
data () {
return {
screen: "staking",
}
},
components: {
ServiceNodeStaking,
ServiceNodeRegistration
}
}
components: {
ServiceNodeStaking,
ServiceNodeRegistration
},
data() {
return {
screen: "staking"
};
}
};
</script>
<style lang="scss">
</style>
<style lang="scss"></style>

View File

@ -1,65 +1,85 @@
<template>
<q-page>
<q-page>
<div class="row q-pt-sm q-mx-md q-mb-sm items-end non-selectable">
<div class="col-5">
{{ $t("titles.transactions") }}
</div>
<div class="col-5">
{{ $t("titles.transactions") }}
</div>
<LokiField class="col-5 q-px-sm" :label="$t('fieldLabels.filter')">
<q-input v-model="tx_filter"
:dark="theme=='dark'"
:placeholder="$t('placeholders.filterTx')"
hide-underline
/>
</LokiField>
<LokiField class="col-2" :label="$t('fieldLabels.filterTransactionType')">
<q-select :dark="theme=='dark'"
v-model="tx_type"
:options="tx_type_options"
hide-underline
/>
</LokiField>
<LokiField class="col-5 q-px-sm" :label="$t('fieldLabels.filter')">
<q-input
v-model="tx_filter"
:dark="theme == 'dark'"
:placeholder="$t('placeholders.filterTx')"
hide-underline
/>
</LokiField>
<LokiField class="col-2" :label="$t('fieldLabels.filterTransactionType')">
<q-select v-model="tx_type" :dark="theme == 'dark'" :options="tx_type_options" hide-underline />
</LokiField>
</div>
<TxList :type="tx_type" :filter="tx_filter" />
</q-page>
</q-page>
</template>
<script>
import { mapState } from "vuex"
import TxList from "components/tx_list"
import LokiField from "components/loki_field"
import { mapState } from "vuex";
import TxList from "components/tx_list";
import LokiField from "components/loki_field";
export default {
data () {
return {
tx_type: "all",
tx_filter: "",
tx_type_options: [
{label: this.$t("strings.transactions.types.all"), value: "all"},
{label: this.$t("strings.transactions.types.incoming"), value: "in"},
{label: this.$t("strings.transactions.types.outgoing"), value: "out"},
{label: this.$t("strings.transactions.types.pending"), value: "all_pending"},
{label: this.$t("strings.transactions.types.miner"), value: "miner"},
{label: this.$t("strings.transactions.types.serviceNode"), value: "snode"},
{label: this.$t("strings.transactions.types.governance"), value: "gov"},
{label: this.$t("strings.transactions.types.stake"), value: "stake"},
{label: this.$t("strings.transactions.types.failed"), value: "failed"},
]
components: {
TxList,
LokiField
},
data() {
return {
tx_type: "all",
tx_filter: "",
tx_type_options: [
{
label: this.$t("strings.transactions.types.all"),
value: "all"
},
{
label: this.$t("strings.transactions.types.incoming"),
value: "in"
},
{
label: this.$t("strings.transactions.types.outgoing"),
value: "out"
},
{
label: this.$t("strings.transactions.types.pending"),
value: "all_pending"
},
{
label: this.$t("strings.transactions.types.miner"),
value: "miner"
},
{
label: this.$t("strings.transactions.types.serviceNode"),
value: "snode"
},
{
label: this.$t("strings.transactions.types.governance"),
value: "gov"
},
{
label: this.$t("strings.transactions.types.stake"),
value: "stake"
},
{
label: this.$t("strings.transactions.types.failed"),
value: "failed"
}
},
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
tx_list: state => state.gateway.wallet.transactions.tx_list
}),
components: {
TxList,
LokiField
}
}
]
};
},
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
tx_list: state => state.gateway.wallet.transactions.tx_list
})
};
</script>
<style lang="scss">
</style>
<style lang="scss"></style>

View File

@ -1,7 +1,5 @@
import axios from "axios"
import axios from "axios";
export default ({
Vue
}) => {
Vue.prototype.$axios = axios
}
export default ({ Vue }) => {
Vue.prototype.$axios = axios;
};

View File

@ -1,4 +1,4 @@
import { Gateway } from "src/gateway/gateway"
import { Gateway } from "src/gateway/gateway";
/* This plugin gets called early in the life-cycle
In the future, we can detect what platform we
@ -9,11 +9,6 @@ import { Gateway } from "src/gateway/gateway"
frontend
*/
export default ({
app,
router,
store,
Vue
}) => {
Vue.prototype.$gateway = new Gateway(app, router)
}
export default ({ app, router, Vue }) => {
Vue.prototype.$gateway = new Gateway(app, router);
};

View File

@ -1,45 +1,47 @@
import VueI18n from "vue-i18n"
import messages from "src/i18n"
import { Quasar } from "quasar"
import VueI18n from "vue-i18n";
import messages from "src/i18n";
import { Quasar } from "quasar";
let i18n
let i18n;
export default ({
app,
Vue
}) => {
Vue.use(VueI18n)
export default ({ app, Vue }) => {
Vue.use(VueI18n);
// Set i18n instance on app
app.i18n = new VueI18n({
locale: "en-us",
fallbackLocale: "en-us",
messages
})
// Set i18n instance on app
app.i18n = new VueI18n({
locale: "en-us",
fallbackLocale: "en-us",
messages
});
i18n = app.i18n
}
i18n = app.i18n;
};
const changeLanguage = (lang) => {
const quasarLang = Quasar.i18n.lang
return new Promise((resolve, reject) => {
import(`src/i18n/${lang}`).then(({ default: messages }) => {
i18n.locale = lang
i18n.setLocaleMessage(lang, messages)
const changeLanguage = lang => {
const quasarLang = Quasar.i18n.lang;
return new Promise((resolve, reject) => {
import(`src/i18n/${lang}`)
.then(({ default: messages }) => {
i18n.locale = lang;
i18n.setLocaleMessage(lang, messages);
// Setting the quasar language is optional
// There may be cases where they don't have the language
import(`quasar-framework/i18n/${lang}`).then(lang => {
quasarLang.set(lang.default)
}).catch(() => {
console.warn(`Failed to set quasar language: ${lang}`)
}).finally(() => {
resolve(lang)
})
}).catch(() => {
reject(new Error("Language not found"))
})
})
}
// Setting the quasar language is optional
// There may be cases where they don't have the language
import(`quasar-framework/i18n/${lang}`)
.then(lang => {
quasarLang.set(lang.default);
})
.catch(() => {
console.warn(`Failed to set quasar language: ${lang}`);
})
.finally(() => {
resolve(lang);
});
})
.catch(() => {
reject(new Error("Language not found"));
});
});
};
export { i18n, changeLanguage }
export { i18n, changeLanguage };

View File

@ -1,19 +1,14 @@
import VueTimeago from "vue-timeago"
export default ({
app,
router,
store,
Vue
}) => {
Vue.use(VueTimeago, {
name: "Timeago",
locale: "en",
locales: {
ru: require("date-fns/locale/ru"),
de: require("date-fns/locale/de"),
fr: require("date-fns/locale/fr"),
es: require("date-fns/locale/es"),
pt: require("date-fns/locale/pt")
}
})
}
import VueTimeago from "vue-timeago";
export default ({ Vue }) => {
Vue.use(VueTimeago, {
name: "Timeago",
locale: "en",
locales: {
ru: require("date-fns/locale/ru"),
de: require("date-fns/locale/de"),
fr: require("date-fns/locale/fr"),
es: require("date-fns/locale/es"),
pt: require("date-fns/locale/pt")
}
});
};

View File

@ -1,5 +1,5 @@
import Vuelidate from "vuelidate"
import Vuelidate from "vuelidate";
export default ({ Vue }) => {
Vue.use(Vuelidate)
}
Vue.use(Vuelidate);
};

View File

@ -1,26 +1,26 @@
import Vue from "vue"
import VueRouter from "vue-router"
import Vue from "vue";
import VueRouter from "vue-router";
import routes from "./routes"
import routes from "./routes";
Vue.use(VueRouter)
Vue.use(VueRouter);
const Router = new VueRouter({
/*
* NOTE! Change Vue Router mode from quasar.conf.js -> build -> vueRouterMode
*
* When going with "history" mode, please also make sure "build.publicPath"
* is set to something other than an empty string.
* Example: "/" instead of ""
*/
/*
* NOTE! Change Vue Router mode from quasar.conf.js -> build -> vueRouterMode
*
* When going with "history" mode, please also make sure "build.publicPath"
* is set to something other than an empty string.
* Example: "/" instead of ""
*/
// Leave as is and change from quasar.conf.js instead!
mode: process.env.VUE_ROUTER_MODE,
base: process.env.VUE_ROUTER_BASE,
scrollBehavior: () => ({
y: 0
}),
routes
})
// Leave as is and change from quasar.conf.js instead!
mode: process.env.VUE_ROUTER_MODE,
base: process.env.VUE_ROUTER_BASE,
scrollBehavior: () => ({
y: 0
}),
routes
});
export default Router
export default Router;

View File

@ -1,127 +1,108 @@
export default [
{
path: "/",
component: () =>
import("layouts/init/loading"),
children: [
{
path: "",
component: () =>
import("pages/init/index")
},
{
path: "/quit",
component: () =>
import("pages/init/quit")
}
]
},
{
path: "/welcome",
component: () =>
import("layouts/init/welcome"),
children: [{
path: "",
component: () =>
import("pages/init/welcome")
}]
},
{
path: "/wallet-select",
component: () =>
import("layouts/wallet-select/main"),
children: [
{
path: "",
name: "wallet-select",
component: () =>
import("pages/wallet-select/index")
},
{
path: "create",
name: "wallet-create",
component: () =>
import("pages/wallet-select/create")
},
{
path: "restore",
name: "wallet-restore",
component: () =>
import("pages/wallet-select/restore")
},
{
path: "import-view-only",
name: "wallet-import-view-only",
component: () =>
import("pages/wallet-select/import-view-only")
},
{
path: "import",
name: "wallet-import",
component: () =>
import("pages/wallet-select/import")
},
{
path: "import-legacy",
name: "wallet-import-legacy",
component: () =>
import("pages/wallet-select/import-legacy")
},
{
path: "created",
name: "wallet-created",
component: () =>
import("pages/wallet-select/created")
},
{
path: "import-old-gui",
name: "wallet-import-old-gui",
component: () =>
import("pages/wallet-select/import-old-gui")
}
]
},
{
path: "/wallet",
component: () =>
import("layouts/wallet/main"),
children: [
{
path: "",
component: () =>
import("pages/wallet/txhistory")
},
{
path: "receive",
component: () =>
import("pages/wallet/receive")
},
{
path: "send",
component: () =>
import("pages/wallet/send")
},
{
path: "addressbook",
component: () =>
import("pages/wallet/addressbook")
},
{
path: "servicenode",
component: () =>
import("pages/wallet/service-node")
},
{
path: "advanced",
component: () =>
import("pages/wallet/advanced")
}
]
},
{
path: "/",
component: () => import("layouts/init/loading"),
children: [
{
path: "",
component: () => import("pages/init/index")
},
{
path: "/quit",
component: () => import("pages/init/quit")
}
]
},
{
path: "/welcome",
component: () => import("layouts/init/welcome"),
children: [
{
path: "",
component: () => import("pages/init/welcome")
}
]
},
{
path: "/wallet-select",
component: () => import("layouts/wallet-select/main"),
children: [
{
path: "",
name: "wallet-select",
component: () => import("pages/wallet-select/index")
},
{
path: "create",
name: "wallet-create",
component: () => import("pages/wallet-select/create")
},
{
path: "restore",
name: "wallet-restore",
component: () => import("pages/wallet-select/restore")
},
{
path: "import-view-only",
name: "wallet-import-view-only",
component: () => import("pages/wallet-select/import-view-only")
},
{
path: "import",
name: "wallet-import",
component: () => import("pages/wallet-select/import")
},
{
path: "import-legacy",
name: "wallet-import-legacy",
component: () => import("pages/wallet-select/import-legacy")
},
{
path: "created",
name: "wallet-created",
component: () => import("pages/wallet-select/created")
},
{
path: "import-old-gui",
name: "wallet-import-old-gui",
component: () => import("pages/wallet-select/import-old-gui")
}
]
},
{
path: "/wallet",
component: () => import("layouts/wallet/main"),
children: [
{
path: "",
component: () => import("pages/wallet/txhistory")
},
{
path: "receive",
component: () => import("pages/wallet/receive")
},
{
path: "send",
component: () => import("pages/wallet/send")
},
{
path: "addressbook",
component: () => import("pages/wallet/addressbook")
},
{
path: "servicenode",
component: () => import("pages/wallet/service-node")
},
{
path: "advanced",
component: () => import("pages/wallet/advanced")
}
]
},
{ // Always leave this as last one
path: "*",
component: () =>
import("pages/404")
}
]
{
// Always leave this as last one
path: "*",
component: () => import("pages/404")
}
];

View File

@ -1,47 +1,44 @@
export const resetWalletData = (state) => {
state.commit("set_wallet_data", {
export const resetWalletData = state => {
state.commit("set_wallet_data", {
status: {
code: 1,
message: null
},
info: {
name: "",
address: "",
height: 0,
balance: 0,
unlocked_balance: 0,
view_only: false
},
secret: {
mnemonic: "",
view_key: "",
spend_key: ""
},
transactions: {
tx_list: []
},
address_list: {
used: [],
unused: [],
address_book: []
}
});
};
status: {
code: 1,
message: null
},
info: {
name: "",
address: "",
height: 0,
balance: 0,
unlocked_balance: 0,
view_only: false
},
secret: {
mnemonic: "",
view_key: "",
spend_key: ""
},
transactions: {
tx_list: []
},
address_list: {
used: [],
unused: [],
address_book: []
}
})
}
export const resetWalletStatus = state => {
state.commit("set_wallet_data", {
status: {
code: 1,
message: null
}
});
};
export const resetWalletStatus = (state) => {
state.commit("set_wallet_data", {
status: {
code: 1,
message: null
}
})
}
export const resetPendingConfig = (state) => {
state.commit("set_app_data", {
pending_config: state.state.app.config
})
}
export const resetPendingConfig = state => {
state.commit("set_app_data", {
pending_config: state.state.app.config
});
};

View File

@ -1,31 +1,31 @@
export const isReady = (state) => {
const { daemons, app } = state.app.config
const config_daemon = daemons[app.net_type]
export const isReady = state => {
const { daemons, app } = state.app.config;
const config_daemon = daemons[app.net_type];
let target_height
if (config_daemon.type === "local") {
target_height = Math.max(state.daemon.info.height, state.daemon.info.target_height)
} else {
target_height = state.daemon.info.height
}
let target_height;
if (config_daemon.type === "local") {
target_height = Math.max(state.daemon.info.height, state.daemon.info.target_height);
} else {
target_height = state.daemon.info.height;
}
return state.wallet.info.height >= target_height - 1
}
return state.wallet.info.height >= target_height - 1;
};
export const isAbleToSend = (state) => {
const { daemons, app } = state.app.config
const config_daemon = daemons[app.net_type]
export const isAbleToSend = state => {
const { daemons, app } = state.app.config;
const config_daemon = daemons[app.net_type];
let target_height
if (config_daemon.type === "local") {
target_height = Math.max(state.daemon.info.height, state.daemon.info.target_height)
} else {
target_height = state.daemon.info.height
}
let target_height;
if (config_daemon.type === "local") {
target_height = Math.max(state.daemon.info.height, state.daemon.info.target_height);
} else {
target_height = state.daemon.info.height;
}
if (config_daemon.type === "local_remote") {
return state.daemon.info.height_without_bootstrap >= target_height && state.wallet.info.height >= target_height - 1
} else {
return state.wallet.info.height >= target_height - 1
}
}
if (config_daemon.type === "local_remote") {
return state.daemon.info.height_without_bootstrap >= target_height && state.wallet.info.height >= target_height - 1;
} else {
return state.wallet.info.height >= target_height - 1;
}
};

View File

@ -1,12 +1,12 @@
import state from "./state"
import * as getters from "./getters"
import * as mutations from "./mutations"
import * as actions from "./actions"
import state from "./state";
import * as getters from "./getters";
import * as mutations from "./mutations";
import * as actions from "./actions";
export default {
namespaced: true,
state,
getters,
mutations,
actions
}
namespaced: true,
state,
getters,
mutations,
actions
};

View File

@ -1,29 +1,29 @@
const objectAssignDeep = require("object-assign-deep")
const objectAssignDeep = require("object-assign-deep");
export const set_app_data = (state, data) => {
state.app = objectAssignDeep.noMutate(state.app, data)
}
state.app = objectAssignDeep.noMutate(state.app, data);
};
export const set_daemon_data = (state, data) => {
state.daemon = objectAssignDeep.noMutate(state.daemon, data)
}
state.daemon = objectAssignDeep.noMutate(state.daemon, data);
};
export const set_wallet_data = (state, data) => {
state.wallet = objectAssignDeep.noMutate(state.wallet, data)
}
state.wallet = objectAssignDeep.noMutate(state.wallet, data);
};
export const set_wallet_list = (state, data) => {
state.wallets = objectAssignDeep.noMutate(state.wallets, data)
}
state.wallets = objectAssignDeep.noMutate(state.wallets, data);
};
export const set_old_gui_import_status = (state, data) => {
state.old_gui_import_status = data
}
state.old_gui_import_status = data;
};
export const set_tx_status = (state, data) => {
state.tx_status = data
}
state.tx_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 = data
}
state.prove_transaction_status = data;
};
export const set_check_transaction_status = (state, data) => {
state.check_transaction_status = data
}
state.check_transaction_status = data;
};

View File

@ -1,111 +1,109 @@
export default {
app: {
status: {
code: 1 // Connecting to backend
},
config: {
appearance: {
theme: "dark"
}
},
pending_config: {
},
remotes: {
}
app: {
status: {
code: 1 // Connecting to backend
},
wallets: {
list: [],
legacy: [],
config: {
appearance: {
theme: "dark"
}
},
pending_config: {},
remotes: {}
},
wallets: {
list: [],
legacy: [],
// List of wallets that are in a sub folder (format of the old GUI)
directories: []
// List of wallets that are in a sub folder (format of the old GUI)
directories: []
},
old_gui_import_status: {
code: 0, // Success
failed_wallets: []
},
wallet: {
status: {
code: 1,
message: null
},
old_gui_import_status: {
code: 0, // Success
failed_wallets: []
info: {
name: "",
address: "",
height: 0,
balance: 0,
unlocked_balance: 0,
view_only: false
},
wallet: {
status: {
code: 1,
message: null
},
info: {
name: "",
address: "",
height: 0,
balance: 0,
unlocked_balance: 0,
view_only: false
},
secret: {
mnemonic: "",
view_key: "",
spend_key: ""
},
transactions: {
tx_list: []
},
address_list: {
used: [],
unused: [],
address_book: []
},
isRPCSyncing: false
secret: {
mnemonic: "",
view_key: "",
spend_key: ""
},
tx_status: {
code: 0,
message: ""
transactions: {
tx_list: []
},
service_node_status: {
stake: {
code: 0,
message: "",
sending: false
},
registration: {
code: 0,
message: "",
sending: false
},
unlock: {
code: 0,
message: "",
sending: false
}
address_list: {
used: [],
unused: [],
address_book: []
},
prove_transaction_status: {
code: 0,
message: "",
state: {}
isRPCSyncing: false
},
tx_status: {
code: 0,
message: ""
},
service_node_status: {
stake: {
code: 0,
message: "",
sending: false
},
check_transaction_status: {
code: 0,
message: "",
state: {}
registration: {
code: 0,
message: "",
sending: false
},
daemon: {
info: {
alt_blocks_count: 0,
cumulative_difficulty: 0,
difficulty: 0,
grey_peerlist_size: 0,
height: 0,
height_without_bootstrap: 0,
incoming_connections_count: 0,
is_ready: false,
outgoing_connections_count: 0,
status: "OK",
target: 240,
target_height: 0,
testnet: false,
top_block_hash: null,
tx_count: 0,
tx_pool_size: 0,
white_peerlist_size: 0
},
connections: [],
bans: [],
tx_pool_backlog: [],
service_nodes: []
unlock: {
code: 0,
message: "",
sending: false
}
}
},
prove_transaction_status: {
code: 0,
message: "",
state: {}
},
check_transaction_status: {
code: 0,
message: "",
state: {}
},
daemon: {
info: {
alt_blocks_count: 0,
cumulative_difficulty: 0,
difficulty: 0,
grey_peerlist_size: 0,
height: 0,
height_without_bootstrap: 0,
incoming_connections_count: 0,
is_ready: false,
outgoing_connections_count: 0,
status: "OK",
target: 240,
target_height: 0,
testnet: false,
top_block_hash: null,
tx_count: 0,
tx_pool_size: 0,
white_peerlist_size: 0
},
connections: [],
bans: [],
tx_pool_backlog: [],
service_nodes: []
}
};

View File

@ -1,14 +1,14 @@
import Vue from "vue"
import Vuex from "vuex"
import Vue from "vue";
import Vuex from "vuex";
import gateway from "./gateway"
import gateway from "./gateway";
Vue.use(Vuex)
Vue.use(Vuex);
const store = new Vuex.Store({
modules: {
gateway
}
})
modules: {
gateway
}
});
export default store
export default store;

View File

@ -1,39 +1,39 @@
/* eslint-disable prefer-promise-reject-errors */
export const greater_than_zero = (input) => {
return input > 0
}
export const greater_than_zero = input => {
return input > 0;
};
export const payment_id = (input) => {
return input.length === 0 || (/^[0-9A-Fa-f]+$/.test(input) && (input.length == 16 || input.length == 64))
}
export const payment_id = input => {
return input.length === 0 || (/^[0-9A-Fa-f]+$/.test(input) && (input.length == 16 || input.length == 64));
};
export const privkey = (input) => {
return input.length === 0 || (/^[0-9A-Fa-f]+$/.test(input) && input.length == 64)
}
export const privkey = input => {
return input.length === 0 || (/^[0-9A-Fa-f]+$/.test(input) && input.length == 64);
};
export const service_node_key = (input) => {
return input.length === 64 && /^[0-9A-Za-z]+$/.test(input)
}
export const service_node_key = input => {
return input.length === 64 && /^[0-9A-Za-z]+$/.test(input);
};
export const address = (input, gateway) => {
if (!(/^[0-9A-Za-z]+$/.test(input))) return false
if (!/^[0-9A-Za-z]+$/.test(input)) return false;
// Validate the address
return new Promise((resolve, reject) => {
gateway.once("validate_address", (data) => {
if (data.address && data.address !== input) {
reject()
} else {
if (data.valid) {
resolve()
} else {
reject()
}
}
})
gateway.send("wallet", "validate_address", {
address: input
})
})
}
// Validate the address
return new Promise((resolve, reject) => {
gateway.once("validate_address", data => {
if (data.address && data.address !== input) {
reject();
} else {
if (data.valid) {
resolve();
} else {
reject();
}
}
});
gateway.send("wallet", "validate_address", {
address: input
});
});
};