Import v1.0.0

This commit is contained in:
mosu forge 2018-09-08 14:44:19 -07:00
commit 94ed4ec316
86 changed files with 26272 additions and 0 deletions

28
.babelrc Normal file
View File

@ -0,0 +1,28 @@
{
"presets": [
[
"@babel/preset-env", {
"modules": false,
"loose": false,
"useBuiltIns": "usage"
}
],
[
"@babel/preset-stage-2", {
"modules": false,
"loose": false,
"useBuiltIns": true,
"decoratorsLegacy": true
}
]
],
"plugins": [
[
"@babel/transform-runtime", {
"polyfill": false,
"regenerator": false
}
]
],
"comments": false
}

9
.editorconfig Normal file
View File

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

1
.eslintignore Normal file
View File

@ -0,0 +1 @@
/dist

47
.eslintrc.js Normal file
View File

@ -0,0 +1,47 @@
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": {
// allow async-await
"generator-star-spacing": "off",
// allow paren-less arrow functions
"arrow-parens": 0,
"one-var": 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
}
}

27
.gitignore vendored Normal file
View File

@ -0,0 +1,27 @@
.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

8
.postcssrc.js Normal file
View File

@ -0,0 +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")
]
}

35
.stylintrc Normal file
View File

@ -0,0 +1,35 @@
{
"blocks": "never",
"brackets": "never",
"colons": "never",
"colors": "always",
"commaSpace": "always",
"commentSpace": "always",
"cssLiteral": "never",
"depthLimit": false,
"duplicates": true,
"efficient": "always",
"extendPref": false,
"globalDupe": true,
"indentPref": 4,
"leadingZero": "never",
"maxErrors": false,
"maxWarnings": false,
"mixed": false,
"namingConvention": false,
"namingConventionStrict": false,
"none": "never",
"noImportant": false,
"parenSpace": "never",
"placeholder": false,
"prefixVarsWithDollar": "always",
"quotePref": "double",
"semicolons": "never",
"sortOrder": false,
"stackedProperties": "never",
"trailingWhitespace": "never",
"universal": "never",
"valid": true,
"zeroUnits": "never",
"zIndexNormalize": false
}

43
LICENSE Normal file
View File

@ -0,0 +1,43 @@
Copyright (c) 2018, Ryo Currency Project
Portions of this software are available under BSD-3 license. Please see ORIGINAL-LICENSE for details
All rights reserved.
Authors and copyright holders give permission for following:
1. Redistribution and use in source and binary forms WITHOUT modification.
2. Modification of the source form for your own personal use.
As long as the following conditions are met:
3. You must not distribute modified copies of the work to third parties. This includes
posting the work online, or hosting copies of the modified work for download.
4. Any derivative version of this work is also covered by this license, including point 8.
5. Neither the name of the copyright holders nor the names of the authors may be
used to endorse or promote products derived from this software without specific
prior written permission.
6. You agree that this licence is governed by and shall be construed in accordance
with the laws of England and Wales.
7. You agree to submit all disputes arising out of or in connection with this licence
to the exclusive jurisdiction of the Courts of England and Wales.
Authors and copyright holders agree that:
8. This licence expires and the work covered by it is released into the
public domain on 1st of February 2019
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

36
ORIGINAL-LICENSE Normal file
View File

@ -0,0 +1,36 @@
This list of conditions and disclaimer is being retained in accordance with condition 1.
Please note that it is not applicable to any changes made by Ryo-Currency Project.
----------------------------------------------------------------------------------------
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors
may be used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
----------------------------------------------------------------------------------------
BSD-licensed version of the code can be found at:
https://github.com/monero-project/monero/tree/e2c39f6b59fcf5c623c814dfefc518ab0b7eca32
https://github.com/ryo-currency/ryo-emergency/tree/9d1f51c453978badad21b2feaca2f4348ab26bfa

120
README.md Normal file
View File

@ -0,0 +1,120 @@
![Ryo Wallet](https://ryo-currency.com/img/ryo-wallet-screenshots/ryo-wallet.png)
Next Generation GUI Wallet for Ryo-currency
Current release: Atom v1.0.0
---
Meet the first release of the new, Electron based, Ryo Wallet: Atom v1.0.0. Being the foundation for further development, this initial release already brings several improvements over previous GUI wallet.
- Wallet switch option.
You can keep several Ryo wallets on one PC, and switch between them easily - just pick your Ryo wallet from the list and enter your password.
- Wallet naming and identifying
Easily identify your wallets - you can give names to them, and each wallet has its own unique identicon image.
- Mixed sync. logic
We took best from both ways of sync: remote (speed) and local (reliability). At wallet startup, you connect to remote node granting quicker operation state. At the same time, you download blockchain files on your hard drive. It will add more reliability to wallet operation. If remote connection fails, you will have local node running. You can also choose to run as normal full node or lite option.
- Power user settings
You can rely on predefined optimum settings, or you can edit settings (list will be expanded):
- Sync. switch (mixed/local/remote)
- Lmdb storage path
- Various ports (daemon, p2p, ryod, remote etc)
- Remote node URL
- Bandwidth utilization (upload/download speed)
- Improved address book
Adding recipients into your address book will let you keep track of who you have sent funds to - you can add recipients of your payments beforehand, or after transactions. Seamless Ryo address validation of fields is built into the address book.
- Lazy load tx history tab
Scroll down and check your transactions list without pagination
- Interface updates
Resizable window with various UX improvements over previous version
- Increased stability and response time
Known issue with stuck processes after closing GUI wallet is now a past history. Overall increased speed and reduced response time of wallet's interface.
- Non latin seed restore
Restore your wallet with non-latin characters (Russian, German, Chinese and other languages)
- Import wallet from old GUI
Ryo wallet will scan default folders used by Lite wallet and GUI wallet and will give ability to restore from key files.
---
![Ryo Wallet Screenshot](https://ryo-currency.com/img/ryo-wallet-screenshots/01-initialize.png)
![Ryo Wallet Screenshot](https://ryo-currency.com/img/ryo-wallet-screenshots/02_wallet-select-1.png)
![Ryo Wallet Screenshot](https://ryo-currency.com/img/ryo-wallet-screenshots/03_wallet-select-2.png)
![Ryo Wallet Screenshot](https://ryo-currency.com/img/ryo-wallet-screenshots/04_wallet-main.png)
![Ryo Wallet Screenshot](https://ryo-currency.com/img/ryo-wallet-screenshots/05_wallet-receive-1.png)
![Ryo Wallet Screenshot](https://ryo-currency.com/img/ryo-wallet-screenshots/06_wallet-receive-2.png)
![Ryo Wallet Screenshot](https://ryo-currency.com/img/ryo-wallet-screenshots/07_wallet-send.png)
![Ryo Wallet Screenshot](https://ryo-currency.com/img/ryo-wallet-screenshots/08_wallet-address-book-1.png)
![Ryo Wallet Screenshot](https://ryo-currency.com/img/ryo-wallet-screenshots/09_wallet-address-book-2.png)
![Ryo Wallet Screenshot](https://ryo-currency.com/img/ryo-wallet-screenshots/10_wallet-address-book-3.png)
![Ryo Wallet Screenshot](https://ryo-currency.com/img/ryo-wallet-screenshots/11_tx-history.png)
![Ryo Wallet Screenshot](https://ryo-currency.com/img/ryo-wallet-screenshots/12_switch-wallet.png)
---
### Building from source
```
npm install -g quasar-cli
git clone https://github.com/ryo-currency/ryo-wallet
cd ryo-wallet
cp /path/to/ryo/binaries/ryod bin/
cp /path/to/ryo/binaries/ryo-wallet-rpc bin/
npm install
quasar build -m electron -t mat
```
---
### LICENSE
Copyright (c) 2018, Ryo Currency Project
Portions of this software are available under BSD-3 license. Please see ORIGINAL-LICENSE for details
All rights reserved.
Authors and copyright holders give permission for following:
1. Redistribution and use in source and binary forms WITHOUT modification.
2. Modification of the source form for your own personal use.
As long as the following conditions are met:
3. You must not distribute modified copies of the work to third parties. This includes
posting the work online, or hosting copies of the modified work for download.
4. Any derivative version of this work is also covered by this license, including point 8.
5. Neither the name of the copyright holders nor the names of the authors may be
used to endorse or promote products derived from this software without specific
prior written permission.
6. You agree that this licence is governed by and shall be construed in accordance
with the laws of England and Wales.
7. You agree to submit all disputes arising out of or in connection with this licence
to the exclusive jurisdiction of the Courts of England and Wales.
Authors and copyright holders agree that:
8. This licence expires and the work covered by it is released into the
public domain on 1st of February 2019
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

0
bin/.gitkeep Normal file
View File

14713
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

54
package.json Normal file
View File

@ -0,0 +1,54 @@
{
"name": "ryo-gui-wallet",
"version": "1.0.0",
"description": "Modern GUI interface for Ryo Currency",
"productName": "Ryo Wallet Atom",
"cordovaId": "com.ryo-currency.ryo-gui-wallet",
"author": "Ryo-currency <contact@ryo-currency.com>",
"private": true,
"scripts": {
"lint": "eslint --ext .js,.vue src",
"test": "echo \"No test specified\" && exit 0"
},
"dependencies": {
"axios": "^0.18.0",
"object-assign-deep": "^0.4.0",
"portscanner": "^2.2.0",
"qrcode.vue": "^1.6.0",
"request": "^2.87.0",
"request-promise": "^4.2.2",
"vue-i18n": "^7.3.3",
"vue-timeago": "^5.0.0",
"vuelidate": "^0.7.4"
},
"devDependencies": {
"babel-eslint": "^8.2.1",
"devtron": "^1.4.0",
"electron": "^2.0.0",
"electron-debug": "^1.5.0",
"electron-devtools-installer": "^2.2.4",
"electron-packager": "^12.1.1",
"eslint": "^4.18.2",
"eslint-config-standard": "^11.0.0",
"eslint-friendly-formatter": "^4.0.1",
"eslint-loader": "^2.0.0",
"eslint-plugin-import": "^2.9.0",
"eslint-plugin-node": "^6.0.1",
"eslint-plugin-promise": "^3.7.0",
"eslint-plugin-standard": "^3.0.1",
"eslint-plugin-vue": "^4.3.0",
"node-sass": "^4.9.3",
"quasar-cli": "^0.16.0",
"sass-loader": "^7.1.0"
},
"engines": {
"node": ">= 8.9.0",
"npm": ">= 5.6.0",
"yarn": ">= 1.6.0"
},
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 10"
]
}

176
quasar.conf.js Normal file
View File

@ -0,0 +1,176 @@
// Configuration for your app
module.exports = function (ctx) {
return {
// app plugins (/src/plugins)
plugins: [
"i18n",
"axios",
"vuelidate",
"gateway",
"timeago"
],
css: [
"app.styl"
],
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) {
/*
cfg.module.rules.push({
enforce: "pre",
test: /\.(js|vue)$/,
loader: "eslint-loader",
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",
"QBtn",
"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"
],
directives: [
"Ripple",
"CloseOverlay"
],
// Quasar plugins
plugins: [
"Notify",
"Loading",
"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: "#027be3",
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: "quasar-app"
}
}
}
}

Binary file not shown.

BIN
src-electron/icons/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

@ -0,0 +1,24 @@
/**
* This file is used specifically and only for development. It installs
* `electron-debug` & `vue-devtools`. There shouldn"t be any need to
* modify this file, but it can be used to extend your development
* environment.
*/
// Install `electron-debug` with `devtron`
require("electron-debug")({
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)
})
})
// Require `main` process to boot app
require("./electron-main")

View File

@ -0,0 +1,129 @@
import { app, ipcMain, BrowserWindow, dialog } from "electron"
import { Backend } from "./modules/backend"
const portscanner = require("portscanner")
/**
* Set `__statics` path to static files in production;
* The reason we are setting it here is that the path needs to be evaluated at runtime
*/
if (process.env.PROD) {
global.__statics = require("path").join(__dirname, "statics").replace(/\\/g, "\\\\")
global.__ryo_bin = require("path").join(__dirname, "..", "bin").replace(/\\/g, "\\\\")
} else {
global.__ryo_bin = require("path").join(process.cwd(), "bin").replace(/\\/g, "\\\\")
}
let mainWindow, backend
let showConfirmClose = true
const portInUse = function(port, callback) {
var server = net.createServer(function(socket) {
socket.write("Echo server\r\n");
socket.pipe(socket);
});
server.listen(port, "127.0.0.1");
server.on("error", function (e) {
callback(true);
});
server.on("listening", function (e) {
server.close();
callback(false);
});
};
function createWindow() {
/**
* Initial window options
*/
mainWindow = new BrowserWindow({
width: 800,
height: 600,
minWidth: 800,
minHeight: 600,
useContentSize: true
})
mainWindow.on("close", (e) => {
if(showConfirmClose) {
e.preventDefault()
mainWindow.webContents.send("confirmClose")
} else {
e.defaultPrevented = false
}
})
ipcMain.on("confirmClose", (e) => {
showConfirmClose = false
if(backend) {
backend.quit().then(() => {
backend = null
app.quit()
})
} else {
app.quit()
}
})
mainWindow.webContents.on("did-finish-load", () => {
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: 12213,
token: buffer.toString("hex")
}
portscanner.checkPortStatus(config.port, "127.0.0.1", (error, status) => {
if(status == "closed") {
mainWindow.webContents.send("initialize", config)
backend = new Backend()
backend.init(config)
} else {
dialog.showMessageBox(mainWindow, {
title: "Startup error",
message: `Ryo Wallet is already open, or port ${config.port} is in use`,
type: "error",
buttons: ["ok"]
}, () => {
showConfirmClose = false
app.quit()
})
}
})
})
})
mainWindow.loadURL(process.env.APP_URL)
}
app.on("ready", createWindow)
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
app.quit()
}
})
app.on("activate", () => {
if (mainWindow === null) {
createWindow()
}
})
app.on("before-quit", () => {
if(backend)
backend.quit()
})
app.on("quit", () => {
})

View File

@ -0,0 +1,91 @@
/*
MIT License
Copyright (c) 2018 Luke Park
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
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;
export class SCEE {
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);
// Encrypt and prepend salt.
let ciphertextAndNonceAndSalt = Buffer.concat([ salt, this.encrypt(new Buffer(plaintext, "utf8"), key) ]);
// Return as base64 string.
return ciphertextAndNonceAndSalt.toString("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);
// 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");
}
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);
// Encrypt and prepend nonce.
let ciphertext = Buffer.concat([ cipher.update(plaintext), cipher.final() ]);
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);
// 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() ]);
}
}

View File

@ -0,0 +1,307 @@
import { Daemon } from "./daemon";
import { WalletRPC } from "./wallet-rpc";
import { SCEE } from "./SCEE-Node";
const WebSocket = require("ws");
const os = require("os");
const fs = require("fs");
const path = require("path");
export class Backend {
constructor() {
this.daemon = null
this.walletd = null
this.wss = null
this.token = null
this.config_dir = null
this.config_file = null
this.config_data = {}
this.scee = new SCEE()
}
init(config) {
if(os.platform() == "win32") {
this.config_dir = "C:\\ProgramData\\ryo";
//this.config_dir = path.join(os.homedir(), "ryo");
} else {
this.config_dir = path.join(os.homedir(), ".ryo");
}
if (!fs.existsSync(this.config_dir)) {
fs.mkdirSync(this.config_dir);
}
if (!fs.existsSync(path.join(this.config_dir, "gui"))) {
fs.mkdirSync(path.join(this.config_dir, "gui"));
}
this.config_file = path.join(this.config_dir, "gui", "config.json");
this.config_data = {
app: {
data_dir: this.config_dir,
ws_bind_port: 12213,
testnet: false
},
daemon: {
type: "local_remote",
remote_host: "geo.ryoblocks.com",
remote_port: 12211,
p2p_bind_ip: "0.0.0.0",
p2p_bind_port: 12210,
rpc_bind_ip: "127.0.0.1",
rpc_bind_port: 12211,
zmq_rpc_bind_ip: "127.0.0.1",
zmq_rpc_bind_port: 12212,
out_peers: -1,
in_peers: -1,
limit_rate_up: -1,
limit_rate_down: -1,
log_level: 0
},
wallet: {
rpc_bind_port: 12214,
log_level: 0
}
}
this.token = config.token
this.wss = new WebSocket.Server({
port: config.port,
maxPayload: Number.POSITIVE_INFINITY
})
this.wss.on("connection", ws => {
ws.on("message", data => this.receive(data));
});
}
send(event, data={}) {
let message = {
event,
data
}
let encrypted_data = this.scee.encryptString(JSON.stringify(message), this.token);
this.wss.clients.forEach(function each(client) {
if (client.readyState === WebSocket.OPEN) {
client.send(encrypted_data)
}
});
}
receive(data) {
let decrypted_data = JSON.parse(this.scee.decryptString(data, this.token));
// route incoming request to either the daemon, wallet, or here
switch (decrypted_data.module) {
case "core":
this.handle(decrypted_data);
break;
case "daemon":
if (this.daemon) {
this.daemon.handle(decrypted_data);
}
break;
case "wallet":
if (this.walletd) {
this.walletd.handle(decrypted_data);
}
break;
}
}
handle(data) {
let params = data.data
switch (data.method) {
case "save_config":
// check if config has changed
let config_changed = false
Object.keys(this.config_data).map(i => {
Object.keys(this.config_data[i]).map(j => {
if(this.config_data[i][j] !== params[i][j])
config_changed = true
})
})
case "save_config_init":
Object.keys(params).map(key => {
this.config_data[key] = Object.assign(this.config_data[key], params[key])
});
fs.writeFile(this.config_file, JSON.stringify(this.config_data, null, 4), 'utf8', () => {
if(data.method == "save_config_init") {
this.startup();
} else if(config_changed) {
this.send("set_app_data", {
config: this.config_data,
pending_config: this.config_data,
});
this.send("settings_changed_reboot")
}
});
break;
case "init":
this.startup();
break;
case "open_explorer":
if(params.type == "tx") {
require("electron").shell.openExternal("https://explorer.ryo-currency.com/tx/"+params.id)
}
break;
case "open_url":
require("electron").shell.openExternal(params.url)
break;
default:
}
}
startup() {
fs.readFile(this.config_file, "utf8", (err,data) => {
if (err) {
this.send("set_app_data", {
status: {
code: -1 // Config not found
},
config: this.config_data,
pending_config: this.config_data,
});
return;
}
let disk_config_data = JSON.parse(data);
// semi-shallow object merge
Object.keys(disk_config_data).map(key => {
this.config_data[key] = Object.assign(this.config_data[key], disk_config_data[key])
});
// here we may want to check if config data is valid, if not also send code -1
// i.e. check ports are integers and > 1024, check that data dir path exists, etc
// save config file back to file, so updated options are stored on disk
fs.writeFile(this.config_file, JSON.stringify(this.config_data, null, 4), 'utf8');
this.send("set_app_data", {
config: this.config_data,
pending_config: this.config_data,
});
if(this.config_data.app.testnet) {
let testnet_dir = path.join(this.config_data.app.data_dir, "testnet")
if (!fs.existsSync(testnet_dir))
fs.mkdirSync(testnet_dir);
let log_dir = path.join(this.config_data.app.data_dir, "testnet", "logs")
if (!fs.existsSync(log_dir))
fs.mkdirSync(log_dir);
} else {
let log_dir = path.join(this.config_data.app.data_dir, "logs")
if (!fs.existsSync(log_dir))
fs.mkdirSync(log_dir);
}
this.daemon = new Daemon(this);
this.walletd = new WalletRPC(this);
this.send("set_app_data", {
status: {
code: 3 // Starting daemon
}
});
this.daemon.checkVersion().then((version) => {
if(version) {
this.send("set_app_data", {
status: {
code: 4,
message: version
}
});
} else {
// daemon not found, probably removed by AV, set to remote node
this.config_data.daemon.type = "remote"
this.send("set_app_data", {
status: {
code: 5
},
config: this.config_data,
pending_config: this.config_data,
});
}
this.daemon.start(this.config_data).then(() => {
this.send("set_app_data", {
status: {
code: 6 // Starting wallet
}
});
this.walletd.start(this.config_data).then(() => {
this.send("set_app_data", {
status: {
code: 7 // Reading wallet list
}
});
this.walletd.listWallets(true)
this.send("set_app_data", {
status: {
code: 0 // Ready
}
});
}).catch(error => {
// send an unrecoverable error to frontend
// wallet-rpc cannot start or be reached
});
}).catch(error => {
// send an unrecoverable error to frontend
// daemon cannot start or be reached
});
}).catch(error => {
// send an unrecoverable error to frontend
// daemon cannot start or be reached
});
});
}
quit() {
return new Promise((resolve, reject) => {
let process = []
if(this.daemon)
process.push(this.daemon.quit())
if(this.walletd)
process.push(this.walletd.quit())
if(this.wss)
this.wss.close();
Promise.all(process).then(() => {
resolve()
})
})
}
}

View File

@ -0,0 +1,241 @@
import child_process from "child_process";
import request from "request-promise";
const fs = require('fs');
const path = require("path");
export class Daemon {
constructor(backend) {
this.backend = backend
this.heartbeat = null
this.id = 0
this.testnet = false
this.local = false // do we have a local daemon ?
}
checkVersion() {
return new Promise((resolve, reject) => {
if (process.platform === "win32") {
if (!fs.existsSync(path.join(__ryo_bin, "ryod.exe")))
resolve(false)
resolve("ryod.exe found")
child_process.exec(path.join(__ryo_bin, "ryod.exe") + " --version", (error, stdout, stderr) => {
if(error)
resolve(false)
resolve(stdout)
})
} else {
if (!fs.existsSync(path.join(__ryo_bin, "ryod")))
resolve(false)
child_process.exec(path.join(__ryo_bin, "ryod") + " --version", {detached: true}, (error, stdout, stderr) => {
if(error)
resolve(false)
resolve(stdout)
})
}
})
}
start(options) {
if(options.daemon.type === "remote") {
this.local = false
// save this info for later RPC calls
this.protocol = "http://"
this.hostname = options.daemon.remote_host
this.port = options.daemon.remote_port
return new Promise((resolve, reject) => {
this.sendRPC("get_info").then((data) => {
if(!data.hasOwnProperty("error")) {
this.startHeartbeat()
resolve()
} else {
reject()
}
})
})
}
return new Promise((resolve, reject) => {
this.local = true
const args = [
"--data-dir", options.app.data_dir,
"--p2p-bind-ip", options.daemon.p2p_bind_ip,
"--p2p-bind-port", options.daemon.p2p_bind_port,
"--rpc-bind-ip", options.daemon.rpc_bind_ip,
"--rpc-bind-port", options.daemon.rpc_bind_port,
"--zmq-rpc-bind-ip", options.daemon.zmq_rpc_bind_ip,
"--zmq-rpc-bind-port", options.daemon.zmq_rpc_bind_port,
"--out-peers", options.daemon.out_peers,
"--in-peers", options.daemon.in_peers,
"--limit-rate-up", options.daemon.limit_rate_up,
"--limit-rate-down", options.daemon.limit_rate_down,
"--log-level", options.daemon.log_level,
];
if(options.app.testnet) {
this.testnet = true
args.push("--testnet")
args.push("--log-file", path.join(options.app.data_dir, "testnet", "logs", "ryod.log"))
} else {
args.push("--log-file", path.join(options.app.data_dir, "logs", "ryod.log"))
}
if(options.daemon.rpc_bind_ip !== "127.0.0.1")
args.push("--confirm-external-bind")
if(options.daemon.type === "local_remote" && !options.app.testnet) {
args.push(
"--bootstrap-daemon-address",
`${options.daemon.remote_host}:${options.daemon.remote_port}`
)
}
if (process.platform === "win32") {
this.daemonProcess = child_process.spawn(path.join(__ryo_bin, "ryod.exe"), args)
} else {
this.daemonProcess = child_process.spawn(path.join(__ryo_bin, "ryod"), args, {
detached: true
})
}
// save this info for later RPC calls
this.protocol = "http://"
this.hostname = options.daemon.rpc_bind_ip
this.port = options.daemon.rpc_bind_port
this.daemonProcess.stdout.on("data", data => process.stdout.write(`Daemon: ${data}`))
this.daemonProcess.on("error", err => process.stderr.write(`Daemon: ${err}`))
this.daemonProcess.on("close", code => process.stderr.write(`Daemon: exited with code ${code}`))
// To let caller know when the daemon is ready
let intrvl = setInterval(() => {
this.sendRPC("get_info").then((data) => {
if(!data.hasOwnProperty("error")) {
this.startHeartbeat()
clearInterval(intrvl);
resolve();
} else {
if(data.error.cause &&
data.error.cause.code === "ECONNREFUSED") {
// Ignore
} else {
clearInterval(intrvl);
reject(error);
}
}
})
}, 1000)
})
}
startHeartbeat() {
clearInterval(this.heartbeat);
this.heartbeat = setInterval(() => {
this.heartbeatAction()
}, this.local ? 2.5 * 1000 : 30 * 1000) // 2.5 seconds for local daemon, 30 seconds for remote
this.heartbeatAction()
}
heartbeatAction() {
let actions = []
if(this.local) {
actions = [
this.getRPC("info"),
this.getRPC("connections"),
this.getRPC("bans"),
this.getRPC("txpool_backlog"),
]
} else {
actions = [
this.getRPC("info"),
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(n.method == "get_info") {
daemon_info.info = n.result
} else 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.backend.send("set_daemon_data", daemon_info)
})
}
sendRPC(method, params={}) {
let id = this.id++
let options = {
forever: true,
json: {
jsonrpc: "2.0",
id: id,
method: method
},
};
if (Object.keys(params).length !== 0) {
options.json.params = params;
}
return request.post(`${this.protocol}${this.hostname}:${this.port}/json_rpc`, options)
.then((response) => {
if(response.hasOwnProperty("error")) {
return {
method: method,
params: params,
error: response.error
}
}
return {
method: method,
params: params,
result: response.result
}
}).catch(error => {
return {
method: method,
params: params,
error: {
code: -1,
message: "Cannot connect to daemon-rpc",
cause: error.cause
}
}
})
}
/**
* 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 => {
resolve()
})
this.daemonProcess.kill()
} else {
resolve()
}
})
}
}

View File

@ -0,0 +1,3 @@
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

14
src/App.vue Normal file
View File

@ -0,0 +1,14 @@
<template>
<div id="q-app">
<router-view />
</div>
</template>
<script>
export default {
name: "App"
}
</script>
<style>
</style>

0
src/components/.gitkeep Normal file
View File

View File

@ -0,0 +1,257 @@
<template>
<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'">
Add address book entry
</q-toolbar-title>
<q-toolbar-title v-else-if="mode=='edit'">
Edit address book entry
</q-toolbar-title>
<q-btn v-if="mode=='edit'" flat no-ripple @click="cancelEdit()" label="Cancel" />
<q-btn class="q-ml-sm" color="primary" @click="save()" label="Save" />
</q-toolbar>
<div>
<q-list no-border>
<q-item>
<q-item-side class="self-start">
<Identicon :address="newEntry.address" />
</q-item-side>
<q-item-main>
<q-field>
<q-input v-model="newEntry.address" float-label="Address"
@blur="$v.newEntry.address.$touch"
:error="$v.newEntry.address.$error"
/>
</q-field>
</q-item-main>
</q-item>
<q-item>
<q-item-main>
<q-field>
<q-input v-model="newEntry.name" float-label="Name" />
</q-field>
</q-item-main>
<q-item-side class="self-start q-pa-sm">
<q-checkbox
v-model="newEntry.starred"
checked-icon="star"
unchecked-icon="star_border"
class="star-entry"
/>
</q-item-side>
</q-item>
<q-item>
<q-item-main>
<q-field>
<q-input v-model="newEntry.payment_id" float-label="Payment ID (optional)"
@blur="$v.newEntry.payment_id.$touch"
:error="$v.newEntry.payment_id.$error"
/>
</q-field>
</q-item-main>
</q-item>
<q-item>
<q-item-main>
<q-field>
<q-input v-model="newEntry.description" type="textarea" float-label="Notes (optional)" />
</q-field>
</q-item-main>
</q-item>
<q-item v-if="mode=='edit'">
<q-item-main>
<q-field>
<q-btn class="float-right" color="red" @click="deleteEntry()" label="Delete" />
</q-field>
</q-item-main>
</q-item>
</q-list>
</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>
Address book details
</q-toolbar-title>
<q-btn class="q-mr-sm"
flat no-ripple
:disable="!is_ready"
@click="edit()" label="Edit" />
<q-btn color="primary" @click="copyAddress" label="Copy address" />
</q-toolbar>
<div class="layout-padding">
<template v-if="entry != null">
<AddressHeader :address="entry.address"
:header="entry.name"
:subheader="entry.address"
:extra="/^0*$/.test(entry.payment_id) ? '' : 'Payment id: '+entry.payment_id"
:extra2="entry.description ? '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">Recent outgoing transactions to this address</span>
</div>
<TxList type="in" :limit="5" :to-outgoing-address="entry.address" />
</div>
</template>
</div>
</q-modal-layout>
</q-modal>
</template>
<script>
import { mapState } from "vuex"
const { clipboard } = require("electron")
import Identicon from "components/identicon"
import AddressHeader from "components/address_header"
import TxList from "components/tx_list"
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
}
}
},
computed: mapState({
is_ready (state) {
return this.$store.getters["gateway/isReady"]
}
}),
validations: {
newEntry: {
address: { required, address },
payment_id: { payment_id }
}
},
methods: {
save () {
this.$v.newEntry.$touch()
if (this.$v.newEntry.address.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: "Address not valid"
})
return
}
if (this.$v.newEntry.payment_id.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: "Payment id not valid"
})
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 () {
},
copyAddress () {
clipboard.writeText(this.entry.address)
this.$q.notify({
type: "positive",
timeout: 1000,
message: "Address copied to clipboard"
})
},
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
}
}
</script>
<style lang="scss">
.address-book-details {
.q-field {
margin: 0 10px 20px;
}
.q-checkbox.star-entry .q-checkbox-icon {
font-size:40px;
margin-left: 10px;
}
}
</style>

View File

@ -0,0 +1,156 @@
<template>
<q-modal v-model="isVisible" maximized :content-css="{padding: '50px'}">
<q-modal-layout>
<q-toolbar slot="header" color="dark" inverted>
<q-btn
flat
round
dense
@click="isVisible = false"
icon="reply"
/>
<q-toolbar-title>
Address details
</q-toolbar-title>
<q-btn flat @click="isQRCodeVisible = true" label="Show QR Code" />
<q-btn class="q-ml-sm" color="primary" @click="copyAddress()" label="Copy address" />
</q-toolbar>
<div class="layout-padding">
<template v-if="address != null">
<AddressHeader :address="address.address"
:header="address.address_index == 0 ? 'Primary address' : 'Sub-address (Index '+address.address_index+')'"
:subheader="address.address"
:extra="'You have '+(address.used?'used':'not used')+' this address'"
/>
<template v-if="address.used">
<div class="row justify-between" style="max-width: 768px">
<div class="infoBox">
<div class="infoBoxContent">
<div class="text"><span>Balance</span></div>
<div class="value"><span><FormatRyo :amount="address.balance" /></span></div>
</div>
</div>
<div class="infoBox">
<div class="infoBoxContent">
<div class="text"><span>Unlocked balance</span></div>
<div class="value"><span><FormatRyo :amount="address.unlocked_balance" /></span></div>
</div>
</div>
<div class="infoBox">
<div class="infoBoxContent">
<div class="text"><span>Number of unspent outputs</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>Balance</span></div>
<div class="value"><span>N/A</span></div>
</div>
</div>
<div class="infoBox">
<div class="infoBoxContent">
<div class="text"><span>Unlocked balance</span></div>
<div class="value"><span>N/A</span></div>
</div>
</div>
<div class="infoBox">
<div class="infoBoxContent">
<div class="text"><span>Number of unspent outputs</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">Recent incoming transactions to this address</span>
</div>
<TxList type="in" :limit="5" :to-incoming-address-index="address.address_index" />
</div>
</template>
</div>
</q-modal-layout>
<template v-if="address != null">
<q-modal v-model="isQRCodeVisible" minimized :content-css="{padding: '25px'}">
<div class="text-center q-mb-sm">
<qrcode-vue :value="address.address" size="240" />
</div>
<q-btn
color="primary"
@click="isQRCodeVisible = false"
label="Close"
/>
</q-modal>
</template>
</q-modal>
</template>
<script>
import { mapState } from "vuex"
const {clipboard} = require("electron")
import AddressHeader from "components/address_header"
import FormatRyo from "components/format_ryo"
import QrcodeVue from "qrcode.vue";
import TxList from "components/tx_list"
export default {
name: "AddressDetails",
computed: mapState({
}),
data () {
return {
isVisible: false,
isQRCodeVisible: false,
address: null
}
},
methods: {
copyAddress() {
clipboard.writeText(this.address.address)
this.$q.notify({
type: "positive",
timeout: 1000,
message: "Address copied to clipboard"
})
}
},
components: {
AddressHeader,
TxList,
FormatRyo,
QrcodeVue
}
}
</script>
<style>
</style>

View File

@ -0,0 +1,75 @@
<template>
<q-item class="address-header">
<q-item-side>
<Identicon :address="address" :size="12" />
</q-item-side>
<q-item-main class="self-start">
<q-item-tile label>{{ header }}</q-item-tile>
<q-item-tile class="monospace break-all" sublabel>{{ subheader }}</q-item-tile>
<q-item-tile v-if="extra" sublabel>{{ extra }}</q-item-tile>
<q-item-tile v-if="extra2" sublabel>{{ extra2 }}</q-item-tile>
</q-item-main>
</q-item>
</template>
<script>
import Identicon from "components/identicon"
export default {
name: "AddressHeader",
props: {
address: {
type: String,
required: true
},
header: {
type: String,
required: true
},
subheader: {
type: String,
required: true
},
extra: {
type: String,
required: false
},
extra2: {
type: String,
required: false
}
},
data () {
return {}
},
components: {
Identicon
}
}
</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;
}
.q-item-main {
.q-item-label {
font-size:2em;
}
}
}
</style>

125
src/components/footer.vue Normal file
View File

@ -0,0 +1,125 @@
<template>
<q-layout-footer>
<div class="status-line monospace">
<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>Remote: {{ daemon.info.height }}</div>
</template>
<div>Wallet: {{ wallet.info.height }} / {{ target_height }} ({{ wallet_pct }}%)</div>
<div>{{ status }}</div>
</div>
<div class="status-bars">
<div v-bind:style="{ width: daemon_pct+'%' }"></div>
<div v-bind:style="{ width: wallet_pct+'%' }"></div>
</div>
</q-layout-footer>
</template>
<script>
import { mapState } from "vuex"
export default {
name: "StatusFooter",
computed: mapState({
config: state => state.gateway.app.config,
daemon: state => state.gateway.daemon,
wallet: state => state.gateway.wallet,
target_height (state) {
if(this.config.daemon.type === "local" && !this.daemon.info.is_ready)
return Math.max(this.daemon.info.height, this.daemon.info.target_height)
else
return this.daemon.info.height
},
daemon_pct (state) {
if(this.config.daemon.type === "local")
return this.daemon_local_pct
return 0
},
daemon_local_pct (state) {
if(this.config.daemon.type === "remote")
return 0
return (100 * this.daemon.info.height_without_bootstrap / this.target_height).toFixed(1)
},
wallet_pct (state) {
return (100 * this.wallet.info.height / this.target_height).toFixed(1)
},
status(state) {
if(this.config.daemon.type === "local") {
if(this.daemon.info.height_without_bootstrap < this.target_height || !this.daemon.info.is_ready) {
return "Syncing..."
} else if(this.wallet.info.height < this.target_height - 1 && this.wallet.info.height != 0) {
return "Scanning..."
} else {
return "Ready"
}
} else {
if(this.wallet.info.height < this.target_height - 1 && this.wallet.info.height != 0) {
return "Scanning..."
} else {
return "Ready"
}
}
return
}
}),
data () {
return {
}
},
}
</script>
<style lang="scss">
.status-line {
margin-bottom: 3px;
div {
display: inline-block;
padding: 0 8px;
}
div:last-child {
float:right;
}
}
.status-bars {
div {
height: 3px;
position: absolute;
bottom: 0;
left: 0;
transition: width 0.5s ease-out;
}
div:first-child {
background-color: goldenrod;
}
div:last-child {
background-color: green;
}
}
.q-layout-footer {
border-top: 1px solid #ccc;
padding-top: 2px;
background: white;
box-shadow: none;
font-size: 10px;
}
</style>

View File

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

View File

@ -0,0 +1,202 @@
<template>
<div class="identicon"
v-bind:style="{backgroundImage: 'url('+img+')', width: 8*size+'px', height: 8*size+'px'}">
</div>
</template>
<script>
export default {
name: "Identicon",
props: {
address: {
default: ""
},
size: {
type: Number,
default: 5
}
},
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="
}
},
created() {
if(this.address && this.isAddressValid(this.address)) {
this.createIcon({
seed: this.address,
scale: this.size
})
} 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: {
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()
}
}
}
</script>
<style>
.identicon {
box-shadow: inset rgba(255, 255, 255, 0.6) 0 2px 2px, inset rgba(0, 0, 0, 0.3) 0 -2px 6px;
border-radius: 2px;
}
</style>

144
src/components/mainmenu.vue Normal file
View File

@ -0,0 +1,144 @@
<template>
<div>
<q-btn class="menu" icon="menu" label="" 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>Switch Wallet</q-item-tile>
</q-item-main>
</q-item>
<q-item v-close-overlay @click.native="openSettings">
<q-item-main>
<q-item-tile label>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>About</q-item-tile>
</q-item-main>
</q-item>
<q-item v-close-overlay @click.native="exit">
<q-item-main>
<q-item-tile label>Exit Ryo GUI Wallet</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">
<img class="q-mb-md" src="statics/ryo-wallet.svg" height="42" />
<p class="q-my-sm">Version: ATOM v1.0.0-0.3.1.0</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://ryo-currency.com/')" href="#">https://ryo-currency.com/</a>
</p>
<p>
<a @click="openExternal('https://t.me/ryocurrency')" href="#">Telegram</a> -
<a @click="openExternal('https://discord.gg/GFQmFtx')" href="#">Discord</a> -
<a @click="openExternal('https://www.reddit.com/r/ryocurrency/')" href="#">Reddit</a>
</p>
</div>
<q-btn
color="primary"
@click="showAbout(false)"
label="Close"
/>
</div>
</q-modal>
</div>
</template>
<script>
import SettingsModal from "components/settings"
export default {
name: "MainMenu",
props: {
disableSwitchWallet: {
type: Boolean,
required: false,
default: false
}
},
computed: {
},
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 () {
this.$q.dialog({
title: "Switch wallet",
message: "Are you sure you want to close the current wallet?",
ok: {
label: "CLOSE"
},
cancel: {
flat: true,
label: "CANCEL"
}
}).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("Are you sure you want to exit?")
}
},
components: {
SettingsModal
}
}
</script>
<style lang="scss">
.about-modal {
padding: 25px;
.external-links {
a {
color: #027be3;
text-decoration: none;
&:hover,
&:active,
&:visited {
text-decoration: underline;
}
}
}
}
</style>

123
src/components/settings.vue Normal file
View File

@ -0,0 +1,123 @@
<template>
<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>
Settings
</q-toolbar-title>
<q-btn color="primary" @click="save" label="Save" />
</q-toolbar>
<div class="sidebar">
<q-list link no-border>
<q-item @click.native="page = 'general'">
<q-item-side icon="settings" />
<q-item-main label="General" />
</q-item>
<q-item @click.native="page = 'peers'">
<q-item-side icon="cloud_queue" />
<q-item-main label="Peers" />
</q-item>
</q-list>
</div>
<div class="body" v-if="page=='general'">
<div class="q-pa-lg">
<SettingsGeneral ref="settingsGeneral"></SettingsGeneral>
</div>
</div>
<div class="body" v-if="page=='peers'">
<q-list link no-border>
<q-list-header>Peer list</q-list-header>
<q-item 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>{{ entry.height }}</q-item-tile>
</q-item-main>
</q-item>
</q-list>
</div>
</q-modal-layout>
</q-modal>
</template>
<script>
import { mapState } from "vuex"
import SettingsGeneral from "components/settings_general"
export default {
name: "SettingsModal",
computed: mapState({
daemon: state => state.gateway.daemon,
pending_config: state => state.gateway.app.pending_config
}),
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
},
showPeerDetails (entry) {
this.$q.dialog({
title: "Peer details",
message: JSON.stringify(entry, null, 2),
ok: {
label: "Ban peer",
color: "negative",
},
cancel: {
label: "Close",
flat: true,
}
}).then(() => {
this.$q.notify("Banned "+entry.address)
}).catch(() => {
})
}
},
components: {
SettingsGeneral
}
}
</script>
<style lang="scss">
.settings-modal {
.sidebar {
/*
position:absolute;
width: 170px;
padding: 10px 0;
*/
display:none;
}
.body {
/*
margin-left:170px;
*/
padding: 10px 0 10px 10px;
}
}
.modal-body.modal-message.modal-scroll {
white-space: pre;
}
</style>

View File

@ -0,0 +1,152 @@
<template>
<div class="settings-general">
<div class="row justify-between q-mb-md">
<div><q-radio v-model="config.daemon.type" val="local_remote" label="Local + Remote Daemon" /></div>
<div><q-radio v-model="config.daemon.type" val="local" label="Local Daemon Only" /></div>
<div><q-radio v-model="config.daemon.type" val="remote" label="Remote Daemon Only" /></div>
</div>
<p v-if="config.daemon.type == 'local_remote'">
Get started quickly with this default option. Wallet will download the full blockchain, but use a remote node while syncing.
</p>
<p v-if="config.daemon.type == 'local'">
Full security, wallet will download the full blockchain. You will not be able to transact until sync is completed.
</p>
<p v-if="config.daemon.type == 'remote'">
Less security, wallet will connect to a remote node to make all transactions.
</p>
<q-field v-if="config.daemon.type != 'remote'">
<div class="row gutter-sm">
<div class="col-8">
<q-input v-model="config.daemon.rpc_bind_ip" float-label="Local Daemon IP" disable />
</div>
<div class="col-4">
<q-input v-model="config.daemon.rpc_bind_port" float-label="Local Daemon Port (RPC)" type="number" :decimals="0" :step="1" min="1024" max="65535" />
</div>
</div>
</q-field>
<q-field v-if="config.daemon.type != 'local'">
<div class="row gutter-sm">
<div class="col-8">
<q-input v-model="config.daemon.remote_host" float-label="Remote Node Host" />
</div>
<div class="col-4">
<q-input v-model="config.daemon.remote_port" float-label="Remote Node Port" type="number" :decimals="0" :step="1" min="1024" max="65535" />
</div>
</div>
</q-field>
<q-field>
<div class="row gutter-sm">
<div class="col-8">
<q-input v-model="config.app.data_dir" stack-label="Data Storage Path" disable />
<input type="file" webkitdirectory directory id="dataPath" v-on:change="setDataPath" ref="fileInput" hidden />
</div>
<div class="col-4">
<q-btn v-on:click="selectPath">Select Location</q-btn>
</div>
</div>
</q-field>
<q-collapsible label="Advanced Options" header-class="non-selectable row reverse advanced-options-label">
<q-field>
<div class="row gutter-sm">
<div class="col-3">
<q-input v-model="config.daemon.log_level" :disable="config.daemon.type == 'remote'"
float-label="Daemon Log Level" type="number" :decimals="0" :step="1" min="0" max="4" />
</div>
<div class="col-3">
<q-input v-model="config.wallet.log_level"
float-label="Wallet Log Level" type="number" :decimals="0" :step="1" min="0" max="4" />
</div>
<div class="col-3">
<q-checkbox v-model="config.app.testnet" label="Testnet" />
</div>
</div>
</q-field>
<q-field>
<div class="row gutter-sm">
<div class="col-3">
<q-input v-model="config.daemon.in_peers" :disable="config.daemon.type == 'remote'"
float-label="Max Incoming Peers" type="number" :decimals="0" :step="1" min="-1" max="65535" />
</div>
<div class="col-3">
<q-input v-model="config.daemon.out_peers" :disable="config.daemon.type == 'remote'"
float-label="Max Outgoing Peers" type="number" :decimals="0" :step="1" min="-1" max="65535" />
</div>
<div class="col-3">
<q-input v-model="config.daemon.limit_rate_up" :disable="config.daemon.type == 'remote'"
float-label="Limit Upload Rate" type="number" suffix="Kb/s" :decimals="0" :step="1" min="-1" max="65535" />
</div>
<div class="col-3">
<q-input v-model="config.daemon.limit_rate_down" :disable="config.daemon.type == 'remote'"
float-label="Limit Download Rate" type="number" suffix="Kb/s" :decimals="0" :step="1" min="-1" max="65535" />
</div>
</div>
</q-field>
<q-field>
<div class="row gutter-sm">
<div class="col-3">
<q-input v-model="config.daemon.p2p_bind_port" :disable="config.daemon.type == 'remote'"
float-label="Daemon P2P Port" type="number" :decimals="0" :step="1" min="1024" max="65535" />
</div>
<div class="col-3">
<q-input v-model="config.daemon.zmq_rpc_bind_port" :disable="config.daemon.type == 'remote'"
float-label="Daemon ZMQ Port" type="number" :decimals="0" :step="1" min="1024" max="65535" />
</div>
<div class="col-3">
<q-input v-model="config.app.ws_bind_port"
float-label="Internal Wallet Port" type="number" :decimals="0" :step="1" min="1024" max="65535" />
</div>
<div class="col-3">
<q-input v-model="config.wallet.rpc_bind_port" :disable="config.daemon.type == 'remote'"
float-label="Wallet RPC Port" type="number" :decimals="0" :step="1" min="1024" max="65535" />
</div>
</div>
</q-field>
</q-collapsible>
</div>
</template>
<script>
import { mapState } from "vuex"
export default {
name: "SettingsGeneral",
computed: mapState({
config: state => state.gateway.app.pending_config,
}),
methods: {
selectPath () {
this.$refs.fileInput.click()
},
setDataPath (file) {
this.config.app.data_dir = file.target.files[0].path
}
}
}
</script>
<style lang="scss">
.settings-general {
.q-field {
margin: 20px 0
}
.q-if-disabled {
cursor: default !important;
.q-input-target {
cursor: default !important;
}
}
.q-item,
.q-collapsible-sub-item {
padding: 0;
}
}
</style>

View File

@ -0,0 +1,258 @@
<template>
<q-modal v-model="isVisible" maximized :content-css="{padding: '50px'}">
<q-modal-layout>
<q-toolbar slot="header" color="dark" inverted>
<q-btn
flat
round
dense
@click="isVisible = false"
icon="reply"
/>
<q-toolbar-title>
Transaction details
</q-toolbar-title>
<q-btn flat class="q-mr-sm" @click="showTxDetails" label="Show tx details" />
<q-btn color="primary" @click="openExplorer" label="View on explorer" />
</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'">
Incoming transaction
</div>
<div :class="'tx-'+tx.type" v-else-if="tx.type=='out'">
Outgoing transaction
</div>
<div :class="'tx-'+tx.type" v-else-if="tx.type=='pool'">
Pending incoming transaction
</div>
<div :class="'tx-'+tx.type" v-else-if="tx.type=='pending'">
Pending outgoing transaction
</div>
<div :class="'tx-'+tx.type" v-else-if="tx.type=='failed'">
Failed transaction
</div>
</div>
<div class="row justify-between" style="max-width: 768px">
<div class="infoBox">
<div class="infoBoxContent">
<div class="text"><span>Amount</span></div>
<div class="value"><span><FormatRyo :amount="tx.amount" /></span></div>
</div>
</div>
<div class="infoBox">
<div class="infoBoxContent">
<div class="text"><span>Fee <template v-if="tx.type=='in'||tx.type=='pool'">(paid by sender)</template></span></div>
<div class="value"><span><FormatRyo :amount="tx.fee" /></span></div>
</div>
</div>
<div class="infoBox">
<div class="infoBoxContent">
<div class="text"><span>Height</span></div>
<div class="value"><span>{{ tx.height }}</span></div>
</div>
</div>
<div class="infoBox">
<div class="infoBoxContent">
<div class="text"><span>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">Transaction id</h6>
<p class="monospace break-all">{{ tx.txid }}</p>
<h6 class="q-mt-xs q-mb-none text-weight-light">Payment id</h6>
<p class="monospace break-all">{{ tx.payment_id }}</p>
<div v-if="tx.type=='in' || tx.type=='pool'">
<q-list no-border>
<q-list-header class="q-px-none">Incoming transaction sent to:</q-list-header>
<q-item class="q-px-none">
<q-item-side>
<Identicon :address="in_tx_address_used.address" />
</q-item-side>
<q-item-main>
<q-item-tile label>{{ 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-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">Outgoing transaction sent to:</q-list-header>
<template v-if="out_destinations">
<q-item class="q-px-none" v-for="destination in out_destinations">
<q-item-side>
<Identicon :address="destination.address" />
</q-item-side>
<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><FormatRyo :amount="destination.amount" /></q-item-tile>
</q-item-main>
</q-item>
</template>
<template v-else>
<q-item class="q-px-none">
<q-item-side>
<Identicon address="" />
</q-item-side>
<q-item-main>
<q-item-tile label>Destination unknown</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="Transaction notes"
type="textarea" rows="2" />
</q-field>
<q-field class="q-mt-sm">
<q-btn
:disable="!is_ready"
@click="saveTxNotes" label="Save tx notes" />
</q-field>
</div>
</q-modal-layout>
</q-modal>
</template>
<script>
import { mapState } from "vuex"
import { date } from "quasar"
const { formatDate } = date
import Identicon from "components/identicon"
import TxTypeIcon from "components/tx_type_icon"
import FormatRyo from "components/format_ryo"
export default {
name: "TxDetails",
computed: mapState({
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 = "Primary address"
} else {
address_index_text = "Sub-address (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++) {
console.log(destination.address, address_book[j].address)
if(destination.address == address_book[j].address) {
destination.name = address_book[j].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: "0000000000000000",
subaddr_index: {major: 0, minor: 0},
timestamp: 0,
txid: "",
type: "",
unlock_time:0
}
}
},
methods: {
showTxDetails () {
this.$q.dialog({
title: "Transaction details",
message: JSON.stringify(this.tx, null, 2),
ok: {
label: "close",
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: "Transaction notes saved"
})
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")
},
},
components: {
Identicon,
TxTypeIcon,
FormatRyo
}
}
</script>
<style>
</style>

202
src/components/tx_list.vue Normal file
View File

@ -0,0 +1,202 @@
<template>
<div>
<template v-if="tx_list.length === 0">
<p class="q-pa-md q-mb-none">No transactions found</p>
</template>
<template v-else>
<q-infinite-scroll :handler="loadMore" ref="scroller">
<q-list link no-border class="tx-list">
<q-item v-for="(tx, index) in tx_list" :key="tx.txid"
@click.native="details(tx)" :class="'tx-'+tx.type">
<q-item-side>
<TxTypeIcon :type="tx.type" />
</q-item-side>
<q-item-main>
<q-item-tile class="monospace ellipsis" label>{{ tx.txid }}</q-item-tile>
<q-item-tile sublabel>{{ formatHeight(tx.height) }}</q-item-tile>
</q-item-main>
<q-item-side>
<q-item-tile label>
<FormatRyo :amount="tx.amount" />
</q-item-tile>
<q-item-tile sublabel>
<timeago :datetime="tx.timestamp*1000" :auto-update="60">
</timeago>
</q-item-tile>
</q-item-side>
</q-item>
<q-spinner-dots slot="message" :size="40"></q-spinner-dots>
</q-list>
</q-infinite-scroll>
</template>
<TxDetails ref="txDetails" />
</div>
</template>
<script>
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 FormatRyo from "components/format_ryo"
export default {
name: "TxList",
data () {
return {
page: 0
}
},
props: {
limit: {
type: Number,
required: false,
default: -1
},
type: {
type: String,
required: false,
default: "all"
},
toOutgoingAddress: {
type: String,
required: false,
default: ""
},
toIncomingAddressIndex: {
type: Number,
required: false,
default: -1
},
},
computed: mapState({
current_height: state => state.gateway.daemon.info.height,
tx_list_all: state => state.gateway.wallet.transactions.tx_list,
tx_list (state) {
let tx_list_filter = this.tx_list_all.filter((tx) => {
let valid = true
if(this.type !== "all" && this.type !== tx.type)
valid = false
if(this.toOutgoingAddress !== "") {
if(tx.hasOwnProperty("destinations")) {
valid = tx.destinations.filter((destination) => { return destination.address === this.toOutgoingAddress }).length;
} else {
valid = false
}
}
if(this.toIncomingAddressIndex !== -1) {
valid = tx.hasOwnProperty("subaddr_index") && tx.subaddr_index.minor == this.toIncomingAddressIndex
}
return valid
})
if(this.limit !== -1) {
tx_list_filter = tx_list_filter.slice(0, this.limit)
} else {
tx_list_filter = tx_list_filter.slice(0, this.page * 24 + 24)
}
return tx_list_filter
},
}),
methods: {
details (tx) {
this.$refs.txDetails.tx = tx;
this.$refs.txDetails.txNotes = tx.note;
this.$refs.txDetails.isVisible = true;
},
formatHeight(height) {
let confirms = this.current_height - height;
if(height == 0)
return "Pending"
if(confirms < 10)
return `Height: ${height} (${confirms} confirm${confirms==1?'':'s'})`
else
return `Height: ${height} (confirmed)`
},
loadMore: function(index, done) {
this.page = index
if(this.limit !== -1 || this.tx_list.length < this.page * 24 + 24)
this.$refs.scroller.stop()
done()
}
},
watch: {
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()
}
}
}
},
components: {
QSpinnerDots,
Identicon,
TxTypeIcon,
TxDetails,
FormatRyo
}
}
</script>
<style lang="scss">
.tx-list {
.q-item.tx-in,
.q-item.tx-pool {
.q-icon {
color: #333;
}
.q-item-label {
color: green;
}
&>div:last-child {
text-align:right;
&>div:first-child {
span {
color: green;
&:before {
content: "+";
color: green;
}
}
}
}
}
.q-item.tx-out,
.q-item.tx-pending {
.q-icon {
color: #333;
}
.q-item-label {
color: purple;
}
&>div:last-child {
text-align:right;
&>div:first-child {
span {
color: purple;
&:before {
content: "-";
color: purple;
}
}
}
}
}
}
</style>

View File

@ -0,0 +1,74 @@
<template>
<div class="tx-icon" v-if="type=='in'">
<q-icon name="call_received" size="40px" class="main-icon" />
<q-tooltip v-if="tooltip" anchor="center right" self="center left" :offset="[10, 10]">
Incoming transaction
</q-tooltip>
</div>
<div class="tx-icon" v-else-if="type=='out'">
<q-icon name="call_made" size="40px" class="main-icon" />
<q-tooltip v-if="tooltip" anchor="center right" self="center left" :offset="[10, 10]">
Outgoing transaction
</q-tooltip>
</div>
<div class="tx-icon" v-else-if="type=='pool'">
<q-icon name="call_received" size="40px" class="main-icon" />
<q-icon name="access_time" size="14px" class="sub-icon" />
<q-tooltip v-if="tooltip" anchor="center right" self="center left" :offset="[10, 10]">
Pending incoming transaction
</q-tooltip>
</div>
<div class="tx-icon" v-else-if="type=='pending'">
<q-icon name="call_made" size="40px" class="main-icon" />
<q-icon name="access_time" size="14px" class="sub-icon" />
<q-tooltip v-if="tooltip" anchor="center right" self="center left" :offset="[10, 10]">
Pending outgoing transaction
</q-tooltip>
</div>
<div class="tx-icon" v-else-if="type=='failed'">
<q-icon name="close" size="40px" class="main-icon" color="red" />
<q-tooltip v-if="tooltip" anchor="center right" self="center left" :offset="[10, 10]">
Failed transaction
</q-tooltip>
</div>
</template>
<script>
export default {
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;
}
.tx-icon .main-icon {
margin: -4px 0 0 -4px;
}
.tx-icon .sub-icon {
width: 14px;
height: 14px;
position:absolute;
bottom: -3px;
right: 0;
}
</style>

173
src/css/app.styl Normal file
View File

@ -0,0 +1,173 @@
// app global css
@font-face {
font-family: 'RobotoMono-Light';
src: url(themes/RobotoMono-Light.ttf)
}
::selection {
background: #63c9f3;
}
::-moz-selection {
background: #63c9f3;
}
h1,h2,h3,h4,h5,h6,
img,
.q-list-header,
.q-layout-header,
.q-popover .q-item-label,
.q-list-link .q-item,
.infoBox .text,
.modal-header,
footer,
{
-webkit-user-select: none !important;
-moz-user-select: none !important;
-ms-user-select: none !important;
user-select: none !important;
}
.monospace {
font-family: 'RobotoMono-Light', monospace
}
.break-all {
word-break: break-all
}
.ellipsis {
text-overflow: ellipsis;
overflow: hidden;
}
.q-field {
margin: 20px 0;
&.q-ma-none {
margin: 0;
}
}
.layout-padding {
padding: 15px 16px !important;
}
.q-item-sublabel {
color: #121212;
}
.advanced-options-label .q-item-side-right {
margin: 0;
text-align: left;
}
.q-layout-header {
background: white;
box-shadow: none;
border-bottom: 1px solid #ddd;
height: 48px;
.q-toolbar-title {
font-size: 16px;
line-height: 48px;
font-weight: 300;
}
&.shift-title {
.q-toolbar-title {
padding-left: 50px;
margin: 0 0 0 16px;
}
}
.q-btn.menu {
height: 48px;
width: 50px;
position: absolute;
top: 0;
z-index: 1;
border-radius: 0;
}
.q-btn.cancel {
position: absolute;
top: 7px;
left: 8px;
z-index: 1;
}
.q-toolbar-inverted {
background: none;
padding-top: 0;
}
.q-tabs {
padding-left: 50px;
.q-tabs-head {
padding: 0;
background: none;
}
.q-tab {
text-transform: none;
&:hover {
background-color: rgba(12,12,12,0.15)
}
.q-icon {
font-size: 22px;
}
}
}
}
.infoBox {
height: 75px;
cursor: default;
position: relative;
overflow: hidden;
margin: 10px 0;
.infoBoxIcon {
position: absolute;
right: 10px;
top: 10px;
width: 64px;
color: #212529;
svg {
width: 60px;
height: 60px;
}
}
.infoBoxContent {
display: inline-block;
.text {
font-size: 13px;
margin-top: 8px;
color: #212529;
text-transform: uppercase;
}
.value {
font-size: 24px;
margin-top: 4px;
font-weight: 800;
color: #212529;
font-weight:300;
}
}
}
.q-loading + .modal {
z-index: 9999 !important;
}

Binary file not shown.

View File

@ -0,0 +1,25 @@
// App Shared Variables
// --------------------------------------------------
// To customize the look and feel of this app, you can override
// the Stylus variables found in Quasar"s source Stylus files. Setting
// variables before Quasar"s Stylus will use these variables rather than
// Quasar"s default Stylus variable values. Stylus variables specific
// to the themes belong in either the variables.ios.styl or variables.mat.styl files.
// Check documentation for full list of Quasar variables
// App Shared Color Variables
// --------------------------------------------------
// It"s highly recommended to change the default colors
// to match your app"s branding.
$primary = #027be3
$secondary = #26A69A
$tertiary = #555
$neutral = #E0E1E2
$positive = #21BA45
$negative = #DB2828
$info = #31CCEC
$warning = #F2C037

View File

@ -0,0 +1,7 @@
// App Shared Variables
// --------------------------------------------------
// Shared Stylus variables go in the common.variables.styl file
@import "common.variables"
// iOS only Quasar variables overwrites
// -----------------------------------------

View File

@ -0,0 +1,7 @@
// App Shared Variables
// --------------------------------------------------
// Shared Stylus variables go in the common.variables.styl file
@import "common.variables"
// Material only Quasar variables overwrites
// -----------------------------------------

91
src/gateway/SCEE-Node.js Normal file
View File

@ -0,0 +1,91 @@
/*
MIT License
Copyright (c) 2018 Luke Park
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
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;
export class SCEE {
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);
// Encrypt and prepend salt.
let ciphertextAndNonceAndSalt = Buffer.concat([ salt, this.encrypt(new Buffer(plaintext, "utf8"), key) ]);
// Return as base64 string.
return ciphertextAndNonceAndSalt.toString("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);
// 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");
}
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);
// Encrypt and prepend nonce.
let ciphertext = Buffer.concat([ cipher.update(plaintext), cipher.final() ]);
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);
// 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() ]);
}
}

123
src/gateway/gateway.js Normal file
View File

@ -0,0 +1,123 @@
import { ipcRenderer } from "electron"
import { Dialog, Loading } from "quasar"
import { SCEE } from "./SCEE-Node";
import * as WebSocket from "ws"
export class Gateway {
constructor(app, router) {
this.app = app
this.router = router
this.token = null
this.scee = new SCEE()
this.closeDialog = false
this.app.store.commit("gateway/set_app_data", {
status: {
code: 1 // Connecting to backend
}
});
ipcRenderer.on("initialize", (event, data) => {
this.token = data.token
this.ws = new WebSocket("ws://127.0.0.1:"+data.port);
this.ws.on("open", () => {this.open()});
this.ws.on("message", (message) => {this.receive(message)});
});
ipcRenderer.on("confirmClose", () => {
this.confirmClose("Are you sure you want to exit?")
});
}
open() {
this.app.store.commit("gateway/set_app_data", {
status: {
code: 2 // Loading config
}
});
this.send("core", "init");
}
confirmClose(msg) {
if(this.closeDialog) {
return
}
this.closeDialog = true
Dialog.create({
title: "Exit",
message: msg,
ok: {
label: "EXIT"
},
cancel: {
flat: true,
label: "CANCEL"
}
}).then(() => {
this.closeDialog = false
Loading.hide()
this.router.replace({ path: "/quit" })
ipcRenderer.send("confirmClose")
}).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);
}
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_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 "set_tx_status":
this.app.store.commit("gateway/set_tx_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("Changes require restart. Would you like to exit now?")
break
}
}
}

7
src/i18n/en-us/index.js Normal file
View File

@ -0,0 +1,7 @@
// This is just an example,
// so you can safely delete all default props below
export default {
failed: "Action failed",
success: "Action was successful"
}

5
src/i18n/index.js Normal file
View File

@ -0,0 +1,5 @@
import enUS from "./en-us"
export default {
"en-us": enUS
}

22
src/index.template.html Normal file
View File

@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="description" content="<%= htmlWebpackPlugin.options.productDescription %>">
<meta name="format-detection" content="telephone=no">
<meta name="msapplication-tap-highlight" content="no">
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width<% if (htmlWebpackPlugin.options.ctx.mode.cordova) { %>, viewport-fit=cover<% } %>">
<title><%= htmlWebpackPlugin.options.productName %></title>
</head>
<body>
<noscript>
This is your fallback content in case JavaScript fails to load.
</noscript>
<!-- DO NOT touch the following <div> -->
<div id="q-app"></div>
<!-- built files will be auto injected here -->
</body>
</html>

118
src/layouts/default.vue Normal file
View File

@ -0,0 +1,118 @@
<template>
<q-layout view="hHh Lpr lFf">
<q-layout-header>
<q-btn-dropdown icon="menu" label="" size="md" flat>
<!-- dropdown content -->
<q-list link>
<q-item>
<q-item-main>
<q-item-tile label>Switch Wallet</q-item-tile>
</q-item-main>
</q-item>
<q-item>
<q-item-main>
<q-item-tile label>Settings</q-item-tile>
</q-item-main>
</q-item>
<q-item>
<q-item-main>
<q-item-tile label>Exit Ryo GUI Wallet</q-item-tile>
</q-item-main>
</q-item>
</q-list>
</q-btn-dropdown>
<q-tabs class="col" align="justify" color="dark" inverted>
<q-tab slot="title"><span><q-icon name="attach_money" /> Wallet</span></q-tab>
<q-tab slot="title"><span><q-icon name="call_received" /> Receive</span></q-tab>
<q-tab slot="title"><span><q-icon name="call_made" /> Send</span></q-tab>
<q-tab slot="title"><span><q-icon name="person" /> Address Book</span></q-tab>
<q-tab slot="title"><span><q-icon name="history" /> TX History</span></q-tab>
</q-tabs>
</q-layout-header>
<q-page-container>
<router-view />
</q-page-container>
<q-layout-footer>
<div class="row">
<div>
Daemon: {{ status }}
</div>
<div>
Height: {{ height }}
</div>
</div>
<q-progress :percentage="progress" stripe animate />
</q-layout-footer>
</q-layout>
</template>
<script>
import {
openURL
} from "quasar"
import {
mapState
} from "vuex"
export default {
name: "LayoutDefault",
data() {
return {
selectedTab: "tab-1",
progress: 40
}
},
computed: {
...mapState({
status: state => state.gateway.info.status,
height: state => state.gateway.info.height
})
},
methods: {
openURL
}
}
</script>
<style>
.q-layout-header {
box-shadow: none;
border-bottom: 1px solid #ddd;
}
.q-layout-header .q-btn-dropdown {
height: 48px;
width: 50px;
position: absolute;
z-index: 1;
border-radius: 0;
}
.q-layout-header .q-btn-dropdown .q-btn-dropdown-arrow {
display: none;
}
.q-layout-header .q-tabs {
padding-left: 50px;
}
.q-layout-header .q-tabs-head {
padding: 0;
}
.q-layout-header .q-tabs .q-tab {
text-transform: none;
}
.q-layout-header .q-tabs .q-icon {
font-size: 22px;
}
</style>

View File

@ -0,0 +1,21 @@
<template>
<q-layout>
<q-page-container>
<router-view />
</q-page-container>
</q-layout>
</template>
<script>
export default {
// name: 'LayoutName',
data () {
return {
}
}
}
</script>
<style>
</style>

View File

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

View File

@ -0,0 +1,104 @@
<template>
<q-layout view="hHh Lpr lFf">
<q-layout-header class="shift-title">
<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=='Ryo'">
<div style="margin-top:7px">
<img src="statics/ryo-wallet.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" />
</q-page-container>
<status-footer />
</q-layout>
</template>
<script>
import { mapState } from "vuex"
import SettingsModal from "components/settings"
import StatusFooter from "components/footer"
import MainMenu from "components/mainmenu"
export default {
data() {
return {
}
},
computed: {
show_menu () {
return this.$route.name === "wallet-select"
},
page_title () {
switch(this.$route.name) {
case "wallet-create":
return "Create new wallet"
break;
case "wallet-restore":
return "Restore wallet from seed"
break;
case "wallet-import":
return "Import wallet from file"
break;
case "wallet-import-legacy":
return "Import wallet from legacy gui"
break;
case "wallet-created":
return "Wallet created/restored"
break;
default:
case "wallet-select":
return "Ryo"
break;
}
}
},
/*
watch: {
"$route": {
deep: true,
handler: function (route) {
this.page_title = route.name
}
}
},
*/
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
}
}
</script>
<style>
</style>

View File

@ -0,0 +1,68 @@
<template>
<q-layout view="hHh Lpr lFf">
<q-layout-header class="shift-title">
<main-menu />
<q-tabs class="col" align="justify" color="dark" inverted>
<q-route-tab to="/wallet" default slot="title">
<span><q-icon name="attach_money" /> Wallet</span>
</q-route-tab>
<q-route-tab to="/wallet/receive" slot="title">
<span><q-icon name="call_received" /> Receive</span>
</q-route-tab>
<q-route-tab to="/wallet/send" slot="title">
<span><q-icon name="call_made" /> Send</span>
</q-route-tab>
<q-route-tab to="/wallet/addressbook" slot="title">
<span><q-icon name="person" /> Address Book</span>
</q-route-tab>
<q-route-tab to="/wallet/txhistory" slot="title">
<span><q-icon name="history" /> TX History</span>
</q-route-tab>
</q-tabs>
</q-layout-header>
<q-page-container>
<keep-alive>
<router-view />
</keep-alive>
</q-page-container>
<status-footer />
</q-layout>
</template>
<script>
import {
openURL
} from "quasar"
import {
mapState
} from "vuex"
import StatusFooter from "components/footer"
import MainMenu from "components/mainmenu"
export default {
name: "LayoutDefault",
data() {
return {
selectedTab: "tab-1",
}
},
methods: {
openURL,
},
components: {
StatusFooter,
MainMenu
}
}
</script>
<style>
</style>

5
src/pages/404.vue Normal file
View File

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

182
src/pages/init/index.vue Normal file
View File

@ -0,0 +1,182 @@
<template>
<q-page padding class="flex flex-center">
<div class="text-center">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="640" viewBox="0 0 859.4011 184.7379">
<defs>
<linearGradient id="b">
<stop offset="0" stop-color="#323232"/>
<stop offset="1" stop-color="#b4b4b4"/>
</linearGradient>
<linearGradient id="a" x2="0" y1="168.1192" y2="286.5673" gradientTransform="translate(82.8992 -169.0231)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#62c9f3"/>
<stop offset="1" stop-color="#3d58b0"/>
</linearGradient>
<linearGradient id="d" x1="138.7479" x2="138.7479" y1="10.1293" y2="104.051" gradientTransform="translate(0 183.1415)" gradientUnits="userSpaceOnUse" xlink:href="#a"/>
<linearGradient id="h" x1="341.1354" x2="341.1354" y1="186.9957" y2="128.2946" gradientTransform="translate(69.6895 183.1415)" gradientUnits="userSpaceOnUse" xlink:href="#b"/>
<linearGradient id="c" x2="0" y1="168.1192" y2="286.5673" gradientTransform="translate(82.8992 -169.0231)" gradientUnits="userSpaceOnUse" xlink:href="#a"/>
<linearGradient id="e" x1="138.7479" x2="138.7479" y1="10.1293" y2="104.051" gradientTransform="translate(0 183.1415)" gradientUnits="userSpaceOnUse" xlink:href="#a"/>
<linearGradient id="f" x1="138.7479" x2="138.7479" y1="10.1293" y2="104.051" gradientTransform="translate(0 183.1415)" gradientUnits="userSpaceOnUse" xlink:href="#a"/>
<linearGradient id="g" x1="138.7479" x2="138.7479" y1="10.1293" y2="104.051" gradientTransform="translate(0 183.1415)" gradientUnits="userSpaceOnUse" xlink:href="#a"/>
<linearGradient id="i" x1="341.1354" x2="341.1354" y1="186.9957" y2="128.2946" gradientTransform="translate(69.6895 183.1415)" gradientUnits="userSpaceOnUse" xlink:href="#b"/>
</defs>
<path fill="url(#c)" d="M41.7535 41.89H75.105v33.0785H41.7535zm-4.2333-4.2334v41.5453h41.8181V37.6565zM58.4295 4.2333c29.9566 0 54.1957 24.2396 54.1957 54.1962 0 29.9566-24.239 54.1957-54.1957 54.1957-29.9566 0-54.1962-24.239-54.1962-54.1957 0-29.9566 24.2396-54.1962 54.1962-54.1962zm0-4.2333C26.185 0 0 26.185 0 58.4295s26.185 58.429 58.4295 58.429 58.429-26.1845 58.429-58.429S90.674 0 58.4295 0z"/>
<g fill="url(#d)" transform="translate(12 -183.1415)">
<path fill="url(#e)" d="M191.1345 287.4374l-15.7334-39.4667q-1.7333.2667-5.4666.2667h-34v39.2h-4.8v-91.7333h40.8q10.5333 0 15.7333 3.6 5.2 3.6 6.5333 9.0666 1.3334 5.3334 1.3334 13.6 0 6.6667-.9334 11.4667-.9333 4.6667-4.2666 8.4-3.3334 3.7333-10 5.3333l16.1333 40.2667zm-21.4667-43.7333q9.3334 0 13.8667-2.6667 4.5333-2.6667 5.8666-7.0667 1.3334-4.5333 1.3334-12 0-7.6-1.2-12-1.0667-4.4-5.2-7.0666-4-2.6667-12.6667-2.6667h-35.7333v43.4667zm63.6625 43.7333v-37.4667l-34.8-54.2666h5.6l31.2 49.2h.9333l31.3334-49.2h5.4666l-34.9333 54.2666v37.4667zm79.7979 1.0666q-17.8666 0-25.0666-3.0666-7.2-3.0667-9.3334-12.1333-2-9.0667-2-31.7334 0-22.6666 2-31.7333 2.1334-9.0666 9.3334-12.1333 7.2-3.0667 25.0666-3.0667 17.8667 0 25.0667 3.0667 7.2 3.0666 9.2 12.1333 2.1333 9.0667 2.1333 31.7333 0 22.6667-2.1333 31.7334-2 9.0666-9.2 12.1333-7.2 3.0667-25.0667 3.0667zm0-4.5333q16.1334 0 21.8667-2 5.8667-2.1333 7.7333-10.2666 2-8.2667 2-30.1334 0-21.8666-2-30-1.8666-8.2666-7.7333-10.2666-5.7333-2.1334-21.8667-2.1334-16.1333 0-22 2.1334-5.7333 2-7.7333 10.2666-1.8667 8.1334-1.8667 30 0 21.8667 1.8667 30.1334 2 8.1333 7.7333 10.2666 5.8667 2 22 2zm153.3563 3.4667l-27.0667-83.0667h-.5333l-27.0667 83.0667h-4.6666l-27.2-91.7333h5.2l24.1333 83.0666h.8l26.6667-83.0666h5.0666l26.6667 83.0666h.8l24.1333-83.0666h4.9333l-27.2 91.7333zm104.6416 0l-12.1333-29.6h-45.8667l-12.1333 29.6h-5.0667l37.8667-91.7333h4.6667l37.8666 91.7333zm-34.6666-84.9333h-.6667l-20.8 50.8h42.1333zm51.6125 84.9333v-91.7333h4.8v87.2h46.6666v4.5333zm62.3541 0v-91.7333h4.8v87.2h46.6667v4.5333z"/>
<path fill="url(#f)" d="M712.7803 287.4374v-91.7333h56.8v4.5333h-52v38.5333h45.7333v4.5334h-45.7333v39.6h52v4.5333z" letter-spacing="-2"/>
<path fill="url(#g)" d="M811.5345 287.4374v-87.2h-31.0667v-4.5333h66.9333v4.5333h-31.0666v87.2z" font-size="133.3333"/>
</g>
<path fill="url(#i)" d="M359.2174 367.2394l-7.28-17.76h-27.52l-7.28 17.76h-3.04l22.72-55.04h2.8l22.72 55.04zm-20.8-50.96h-.4l-12.48 30.48h25.28zm41.0388 50.96v-52.32h-18.64v-2.72h40.16v2.72h-18.64v52.32zm53.7625.64q-10.72 0-15.04-1.84-4.32-1.84-5.6-7.28-1.2-5.44-1.2-19.04 0-13.6 1.2-19.04 1.28-5.44 5.6-7.28 4.32-1.84 15.04-1.84 10.72 0 15.04 1.84 4.32 1.84 5.52 7.28 1.28 5.44 1.28 19.04 0 13.6-1.28 19.04-1.2 5.44-5.52 7.28-4.32 1.84-15.04 1.84zm0-2.72q9.68 0 13.12-1.2 3.52-1.28 4.64-6.16 1.2-4.96 1.2-18.08 0-13.12-1.2-18-1.12-4.96-4.64-6.16-3.44-1.28-13.12-1.28t-13.2 1.28q-3.44 1.2-4.64 6.16-1.12 4.88-1.12 18 0 13.12 1.12 18.08 1.2 4.88 4.64 6.16 3.52 1.2 13.2 1.2zm90.205 2.08v-49.44h-.32l-22.96 49.44h-2.56l-22.96-49.44h-.32v49.44h-2.8v-55.04h3.68l23.52 50.8h.4l23.6-50.8h3.6v55.04z" transform="translate(9.5 -183.1415)"/>
</svg>
<!--<q-spinner color="secondary" :size="30" />-->
<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"><title>619</title><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"><title>969</title><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="startup-version monospace">
{{ version }}
</div>
</div>
</q-page>
</template>
<script>
import { mapState } from "vuex"
export default {
data() {
return {
message: "",
version: ""
}
},
computed: mapState({
status: state => state.gateway.app.status,
}),
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 = "Connecting to backend"
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 = "Loading settings"
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 = "Starting daemon"
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: ryod not found, using remote node"
})
break;
case 6:
this.message = "Starting wallet"
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 = "Reading wallet list"
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
}
}
}
</script>
<style lang="scss">
.startup-version {
position: absolute;
left: 0;
bottom: 0;
width: 100%;
text-align:center;
color: #999;
font-size:0.8em;
}
.startup-icons {
&>div {
display: inline-block;
margin: 0 15px;
color: lightgrey;
g,path {
fill: lightgrey;
}
}
.solid {
color: royalblue;
g,path {
fill: royalblue;
}
}
.pulse {
color:black;
opacity: 0.6;
animation: fade 2s infinite;
g,path {
fill:black;
}
}
}
@keyframes fade {
0%,100% { opacity: 0.3 }
50% { opacity: 0.6 }
}
</style>

50
src/pages/init/quit.vue Normal file
View File

@ -0,0 +1,50 @@
<template>
<q-page padding class="flex flex-center">
<div class="text-center">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="640" viewBox="0 0 859.4011 184.7379">
<defs>
<linearGradient id="b">
<stop offset="0" stop-color="#323232"/>
<stop offset="1" stop-color="#b4b4b4"/>
</linearGradient>
<linearGradient id="a" x2="0" y1="168.1192" y2="286.5673" gradientTransform="translate(82.8992 -169.0231)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#62c9f3"/>
<stop offset="1" stop-color="#3d58b0"/>
</linearGradient>
<linearGradient id="d" x1="138.7479" x2="138.7479" y1="10.1293" y2="104.051" gradientTransform="translate(0 183.1415)" gradientUnits="userSpaceOnUse" xlink:href="#a"/>
<linearGradient id="h" x1="341.1354" x2="341.1354" y1="186.9957" y2="128.2946" gradientTransform="translate(69.6895 183.1415)" gradientUnits="userSpaceOnUse" xlink:href="#b"/>
<linearGradient id="c" x2="0" y1="168.1192" y2="286.5673" gradientTransform="translate(82.8992 -169.0231)" gradientUnits="userSpaceOnUse" xlink:href="#a"/>
<linearGradient id="e" x1="138.7479" x2="138.7479" y1="10.1293" y2="104.051" gradientTransform="translate(0 183.1415)" gradientUnits="userSpaceOnUse" xlink:href="#a"/>
<linearGradient id="f" x1="138.7479" x2="138.7479" y1="10.1293" y2="104.051" gradientTransform="translate(0 183.1415)" gradientUnits="userSpaceOnUse" xlink:href="#a"/>
<linearGradient id="g" x1="138.7479" x2="138.7479" y1="10.1293" y2="104.051" gradientTransform="translate(0 183.1415)" gradientUnits="userSpaceOnUse" xlink:href="#a"/>
<linearGradient id="i" x1="341.1354" x2="341.1354" y1="186.9957" y2="128.2946" gradientTransform="translate(69.6895 183.1415)" gradientUnits="userSpaceOnUse" xlink:href="#b"/>
</defs>
<path fill="url(#c)" d="M41.7535 41.89H75.105v33.0785H41.7535zm-4.2333-4.2334v41.5453h41.8181V37.6565zM58.4295 4.2333c29.9566 0 54.1957 24.2396 54.1957 54.1962 0 29.9566-24.239 54.1957-54.1957 54.1957-29.9566 0-54.1962-24.239-54.1962-54.1957 0-29.9566 24.2396-54.1962 54.1962-54.1962zm0-4.2333C26.185 0 0 26.185 0 58.4295s26.185 58.429 58.4295 58.429 58.429-26.1845 58.429-58.429S90.674 0 58.4295 0z"/>
<g fill="url(#d)" transform="translate(12 -183.1415)">
<path fill="url(#e)" d="M191.1345 287.4374l-15.7334-39.4667q-1.7333.2667-5.4666.2667h-34v39.2h-4.8v-91.7333h40.8q10.5333 0 15.7333 3.6 5.2 3.6 6.5333 9.0666 1.3334 5.3334 1.3334 13.6 0 6.6667-.9334 11.4667-.9333 4.6667-4.2666 8.4-3.3334 3.7333-10 5.3333l16.1333 40.2667zm-21.4667-43.7333q9.3334 0 13.8667-2.6667 4.5333-2.6667 5.8666-7.0667 1.3334-4.5333 1.3334-12 0-7.6-1.2-12-1.0667-4.4-5.2-7.0666-4-2.6667-12.6667-2.6667h-35.7333v43.4667zm63.6625 43.7333v-37.4667l-34.8-54.2666h5.6l31.2 49.2h.9333l31.3334-49.2h5.4666l-34.9333 54.2666v37.4667zm79.7979 1.0666q-17.8666 0-25.0666-3.0666-7.2-3.0667-9.3334-12.1333-2-9.0667-2-31.7334 0-22.6666 2-31.7333 2.1334-9.0666 9.3334-12.1333 7.2-3.0667 25.0666-3.0667 17.8667 0 25.0667 3.0667 7.2 3.0666 9.2 12.1333 2.1333 9.0667 2.1333 31.7333 0 22.6667-2.1333 31.7334-2 9.0666-9.2 12.1333-7.2 3.0667-25.0667 3.0667zm0-4.5333q16.1334 0 21.8667-2 5.8667-2.1333 7.7333-10.2666 2-8.2667 2-30.1334 0-21.8666-2-30-1.8666-8.2666-7.7333-10.2666-5.7333-2.1334-21.8667-2.1334-16.1333 0-22 2.1334-5.7333 2-7.7333 10.2666-1.8667 8.1334-1.8667 30 0 21.8667 1.8667 30.1334 2 8.1333 7.7333 10.2666 5.8667 2 22 2zm153.3563 3.4667l-27.0667-83.0667h-.5333l-27.0667 83.0667h-4.6666l-27.2-91.7333h5.2l24.1333 83.0666h.8l26.6667-83.0666h5.0666l26.6667 83.0666h.8l24.1333-83.0666h4.9333l-27.2 91.7333zm104.6416 0l-12.1333-29.6h-45.8667l-12.1333 29.6h-5.0667l37.8667-91.7333h4.6667l37.8666 91.7333zm-34.6666-84.9333h-.6667l-20.8 50.8h42.1333zm51.6125 84.9333v-91.7333h4.8v87.2h46.6666v4.5333zm62.3541 0v-91.7333h4.8v87.2h46.6667v4.5333z"/>
<path fill="url(#f)" d="M712.7803 287.4374v-91.7333h56.8v4.5333h-52v38.5333h45.7333v4.5334h-45.7333v39.6h52v4.5333z" letter-spacing="-2"/>
<path fill="url(#g)" d="M811.5345 287.4374v-87.2h-31.0667v-4.5333h66.9333v4.5333h-31.0666v87.2z" font-size="133.3333"/>
</g>
<path fill="url(#i)" d="M359.2174 367.2394l-7.28-17.76h-27.52l-7.28 17.76h-3.04l22.72-55.04h2.8l22.72 55.04zm-20.8-50.96h-.4l-12.48 30.48h25.28zm41.0388 50.96v-52.32h-18.64v-2.72h40.16v2.72h-18.64v52.32zm53.7625.64q-10.72 0-15.04-1.84-4.32-1.84-5.6-7.28-1.2-5.44-1.2-19.04 0-13.6 1.2-19.04 1.28-5.44 5.6-7.28 4.32-1.84 15.04-1.84 10.72 0 15.04 1.84 4.32 1.84 5.52 7.28 1.28 5.44 1.28 19.04 0 13.6-1.28 19.04-1.2 5.44-5.52 7.28-4.32 1.84-15.04 1.84zm0-2.72q9.68 0 13.12-1.2 3.52-1.28 4.64-6.16 1.2-4.96 1.2-18.08 0-13.12-1.2-18-1.12-4.96-4.64-6.16-3.44-1.28-13.12-1.28t-13.2 1.28q-3.44 1.2-4.64 6.16-1.12 4.88-1.12 18 0 13.12 1.12 18.08 1.2 4.88 4.64 6.16 3.52 1.2 13.2 1.2zm90.205 2.08v-49.44h-.32l-22.96 49.44h-2.56l-22.96-49.44h-.32v49.44h-2.8v-55.04h3.68l23.52 50.8h.4l23.6-50.8h3.6v55.04z" transform="translate(9.5 -183.1415)"/>
</svg>
<div class="q-mt-xl q-mb-lg">
<q-spinner color="primary" :size="30" />
</div>
<div class="message">
Closing...
</div>
</div>
</q-page>
</template>
<script>
export default {
}
</script>
<style lang="scss">
</style>

173
src/pages/init/welcome.vue Normal file
View File

@ -0,0 +1,173 @@
<template>
<q-page>
<q-stepper class="no-shadow" ref="stepper">
<q-step default title="Welcome">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 859.4011 116.8585" height="64">
<defs>
<linearGradient id="a" x2="0" y1="168.1192" y2="286.5673" gradientTransform="translate(82.8992 -169.0231)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#62c9f3"/>
<stop offset="1" stop-color="#3d58b0"/>
</linearGradient>
<linearGradient id="c" x2="0" y1="168.1192" y2="286.5673" gradientTransform="translate(82.8992 -169.0231)" gradientUnits="userSpaceOnUse" xlink:href="#a"/>
<linearGradient id="e" x1="138.7479" x2="138.7479" y1="10.1293" y2="104.051" gradientTransform="translate(0 183.1415)" gradientUnits="userSpaceOnUse" xlink:href="#a"/>
<linearGradient id="f" x1="138.7479" x2="138.7479" y1="10.1293" y2="104.051" gradientTransform="translate(0 183.1415)" gradientUnits="userSpaceOnUse" xlink:href="#a"/>
<linearGradient id="g" x1="138.7479" x2="138.7479" y1="10.1293" y2="104.051" gradientTransform="translate(0 183.1415)" gradientUnits="userSpaceOnUse" xlink:href="#a"/>
</defs>
<path fill="url(#c)" d="M41.7535 41.89H75.105v33.0785H41.7535zm-4.2333-4.2334v41.5453h41.8181V37.6565zM58.4295 4.2333c29.9566 0 54.1957 24.2396 54.1957 54.1962 0 29.9566-24.239 54.1957-54.1957 54.1957-29.9566 0-54.1962-24.239-54.1962-54.1957 0-29.9566 24.2396-54.1962 54.1962-54.1962zm0-4.2333C26.185 0 0 26.185 0 58.4295s26.185 58.429 58.4295 58.429 58.429-26.1845 58.429-58.429S90.674 0 58.4295 0z"/>
<g transform="translate(12 -183.1415)">
<path fill="url(#e)" d="M191.1345 287.4374l-15.7334-39.4667q-1.7333.2667-5.4666.2667h-34v39.2h-4.8v-91.7333h40.8q10.5333 0 15.7333 3.6 5.2 3.6 6.5333 9.0666 1.3334 5.3334 1.3334 13.6 0 6.6667-.9334 11.4667-.9333 4.6667-4.2666 8.4-3.3334 3.7333-10 5.3333l16.1333 40.2667zm-21.4667-43.7333q9.3334 0 13.8667-2.6667 4.5333-2.6667 5.8666-7.0667 1.3334-4.5333 1.3334-12 0-7.6-1.2-12-1.0667-4.4-5.2-7.0666-4-2.6667-12.6667-2.6667h-35.7333v43.4667zm63.6625 43.7333v-37.4667l-34.8-54.2666h5.6l31.2 49.2h.9333l31.3334-49.2h5.4666l-34.9333 54.2666v37.4667zm79.7979 1.0666q-17.8666 0-25.0666-3.0666-7.2-3.0667-9.3334-12.1333-2-9.0667-2-31.7334 0-22.6666 2-31.7333 2.1334-9.0666 9.3334-12.1333 7.2-3.0667 25.0666-3.0667 17.8667 0 25.0667 3.0667 7.2 3.0666 9.2 12.1333 2.1333 9.0667 2.1333 31.7333 0 22.6667-2.1333 31.7334-2 9.0666-9.2 12.1333-7.2 3.0667-25.0667 3.0667zm0-4.5333q16.1334 0 21.8667-2 5.8667-2.1333 7.7333-10.2666 2-8.2667 2-30.1334 0-21.8666-2-30-1.8666-8.2666-7.7333-10.2666-5.7333-2.1334-21.8667-2.1334-16.1333 0-22 2.1334-5.7333 2-7.7333 10.2666-1.8667 8.1334-1.8667 30 0 21.8667 1.8667 30.1334 2 8.1333 7.7333 10.2666 5.8667 2 22 2zm153.3563 3.4667l-27.0667-83.0667h-.5333l-27.0667 83.0667h-4.6666l-27.2-91.7333h5.2l24.1333 83.0666h.8l26.6667-83.0666h5.0666l26.6667 83.0666h.8l24.1333-83.0666h4.9333l-27.2 91.7333zm104.6416 0l-12.1333-29.6h-45.8667l-12.1333 29.6h-5.0667l37.8667-91.7333h4.6667l37.8666 91.7333zm-34.6666-84.9333h-.6667l-20.8 50.8h42.1333zm51.6125 84.9333v-91.7333h4.8v87.2h46.6666v4.5333zm62.3541 0v-91.7333h4.8v87.2h46.6667v4.5333z"/>
<path fill="url(#f)" d="M712.7803 287.4374v-91.7333h56.8v4.5333h-52v38.5333h45.7333v4.5334h-45.7333v39.6h52v4.5333z"/>
<path fill="url(#g)" d="M811.5345 287.4374v-87.2h-31.0667v-4.5333h66.9333v4.5333h-31.0666v87.2z"/>
</g>
</svg>
<div>Version: ATOM v1.0.0-0.3.1.0</div>
<h6 class="q-mb-md" style="font-weight: 300">Select language:</h6>
<div class="row">
<q-btn flat class="language-item">
<div class="language-item-circle">EN</div> English
</q-btn>
</div>
<p class="q-mt-md">More languages coming soon</p>
</q-step>
<q-step title="Configure">
<SettingsGeneral ref="settingsGeneral"></SettingsGeneral>
</q-step>
<q-step title="Review">
<h2 class="q-mt-none q-mb-none text-weight-thin">You're almost set!</h2>
<h6 class="q-mb-md q-mt-md" style="font-weight: 300">Review settings:</h6>
<p>You are using a
<template v-if="pending_config.daemon.type == 'local'">
<code>local node</code>
</template>
<template v-if="pending_config.daemon.type == 'local_remote'">
<code>local + remote node</code>
</template>
<template v-if="pending_config.daemon.type == 'remote'">
<code>remote node</code>
</template>
<template v-if="pending_config.app.testnet">
<code>on testnet</code>
</template>
and will store data in
<code>{{ pending_config.app.data_dir }}</code>
</p>
<p>Press next to get started!</p>
</q-step>
</q-stepper>
<q-layout-footer class="no-shadow q-pa-sm">
<div class="row justify-end">
<div>
<q-btn
flat
@click="clickPrev()"
label="Back"
/>
</div>
<div>
<q-btn
class="q-ml-sm"
color="primary"
@click="clickNext()"
label="Next"
/>
</div>
</div>
</q-layout-footer>
</q-page>
</template>
<script>
import { mapState } from "vuex"
import SettingsGeneral from "components/settings_general"
export default {
computed: mapState({
pending_config: state => state.gateway.app.pending_config
}),
mounted () {
// set add status back to 2
this.$store.commit("gateway/set_app_data", {
status: {
code: 2 // Loading config
}
});
},
methods: {
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();
},
},
components: {
SettingsGeneral
}
}
</script>
<style lang="scss">
.language-item {
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;
}
}
.q-stepper-header {
min-height: 50px;
.q-stepper-tab {
padding-top: 0;
padding-bottom: 0;
}
}
footer {
background:white;
}
</style>

View File

@ -0,0 +1,148 @@
<template>
<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"
/>
</q-field>
<q-field>
<q-select
v-model="wallet.language"
float-label="Seed language"
:options="languageOptions"
/>
</q-field>
<q-field>
<div class="row gutter-md">
<div><q-radio v-model="wallet.type" val="long" label="Long address" /></div>
<div><q-radio v-model="wallet.type" val="kurz" label="Short (kurz) address" /></div>
</div>
</q-field>
<p v-if="wallet.type == 'long'">
Create both public/private view & spend keys. Allows creation of view-only wallets.
</p>
<p v-if="wallet.type == 'kurz'">
Create shorter style address with only private view & spend keys. Does NOT support view-only wallets.
</p>
<q-field>
<q-input v-model="wallet.password" type="password" float-label="Password" />
</q-field>
<q-field>
<q-input v-model="wallet.password_confirm" type="password" float-label="Confirm Password" />
</q-field>
<q-btn color="primary" @click="create" label="Create wallet" />
</div>
</q-page>
</template>
<script>
import { required } from "vuelidate/lib/validators"
import { mapState } from "vuex"
export default {
data () {
return {
wallet: {
name: "",
language: "English",
type: "long",
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"}
]
}
},
computed: mapState({
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: "Enter a wallet name"
})
return
}
if(this.wallet.password != this.wallet.password_confirm) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: "Passwords do not match"
})
return
}
this.$q.loading.show({
delay: 0
})
this.$gateway.send("wallet", "create_wallet", this.wallet);
},
cancel() {
this.$router.replace({ path: "/wallet-select" });
}
}
}
</script>
<style>
</style>

View File

@ -0,0 +1,53 @@
<template>
<q-page padding>
<AddressHeader :address="info.address" :header="info.name" :subheader="info.address" />
<h6 class="q-mb-xs">Seed words</h6>
<p>{{ secret.mnemonic }}</p>
<template v-if="secret.view_key != secret.spend_key">
<h6 class="q-mb-xs">View key</h6>
<p>{{ secret.view_key }}</p>
</template>
<h6 class="q-mb-xs">Spend key</h6>
<p>{{ secret.spend_key }}</p>
<q-btn class="q-mt-lg" color="primary" @click="open" label="Open wallet" />
</q-page>
</template>
<script>
import { mapState } from "vuex"
import AddressHeader from "components/address_header"
export default {
computed: mapState({
info: state => state.gateway.wallet.info,
secret: state => state.gateway.wallet.secret,
}),
methods: {
open() {
setTimeout(() => {
this.$store.commit("gateway/set_wallet_data", {
secret: {
mnemonic: "",
spend_key: "",
view_key: ""
}
})
}, 500)
this.$router.replace({ path: "/wallet" });
}
},
components: {
AddressHeader,
}
}
</script>
<style>
</style>

View File

@ -0,0 +1,134 @@
<template>
<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"
/>
</q-field>
<q-field>
<div class="row gutter-sm">
<div class="col-12">
<q-input v-model="wallet_path" stack-label="Wallet file" disable />
</div>
</div>
</q-field>
<q-field>
<q-input v-model="wallet.password" type="password" float-label="Password" />
</q-field>
<q-field>
<q-input v-model="wallet.password_confirm" type="password" float-label="Confirm Password" />
</q-field>
<q-btn color="primary" @click="import_wallet" label="Import wallet" />
</div>
</q-page>
</template>
<script>
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({
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: "Enter a wallet name"
})
return
}
if(this.wallet.password != this.wallet.password_confirm) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: "Passwords do not match"
})
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>

View File

@ -0,0 +1,128 @@
<template>
<q-page>
<div class="q-mx-md">
<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"
/>
</q-field>
<q-field>
<div class="row gutter-sm">
<div class="col-8">
<q-input v-model="wallet.path" stack-label="Wallet file" disable />
<input type="file" id="walletPath" v-on:change="setWalletPath" ref="fileInput" hidden />
</div>
<div class="col-4">
<q-btn v-on:click="selectFile">Select wallet file</q-btn>
</div>
</div>
</q-field>
<q-field>
<q-input v-model="wallet.password" type="password" float-label="Password" />
</q-field>
<q-field>
<q-input v-model="wallet.password_confirm" type="password" float-label="Confirm Password" />
</q-field>
<q-btn color="primary" @click="import_wallet" label="Import wallet" />
</div>
</q-page>
</template>
<script>
import { required } from "vuelidate/lib/validators"
import { mapState } from "vuex"
export default {
data () {
return {
wallet: {
name: "",
path: "",
password: "",
password_confirm: ""
},
}
},
computed: mapState({
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: {
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.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: "Enter a wallet name"
})
return
}
if(this.wallet.password != this.wallet.password_confirm) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: "Passwords do not match"
})
return
}
this.$q.loading.show({
delay: 0
})
this.$gateway.send("wallet", "import_wallet", this.wallet);
},
cancel() {
this.$router.replace({ path: "/wallet-select" });
}
}
}
</script>
<style>
</style>

View File

@ -0,0 +1,127 @@
<template>
<q-page>
<q-list link no-border>
<template v-if="wallets.list.length">
<q-list-header>Open wallet</q-list-header>
<q-item v-for="wallet in wallets.list" @click.native="openWallet(wallet)">
<q-item-side>
<Identicon :address="wallet.address" />
</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>
<q-item-separator />
</template>
<q-item @click.native="createNewWallet()">
<!--<q-item-side avatar="statics/guy-avatar.png" />-->
<q-item-main label="Create new wallet" />
</q-item>
<q-item @click.native="restoreWallet()">
<!--<q-item-side avatar="statics/guy-avatar.png" />-->
<q-item-main label="Restore wallet from seed" />
</q-item>
<q-item @click.native="importWallet()">
<!--<q-item-side avatar="statics/guy-avatar.png" />-->
<q-item-main label="Import wallet from file" />
</q-item>
<template v-if="wallets.legacy.length">
<q-item @click.native="importLegacyWallet()">
<!--<q-item-side avatar="statics/guy-avatar.png" />-->
<q-item-main label="Import wallet from legacy gui" />
</q-item>
</template>
</q-list>
</q-page>
</template>
<script>
import { mapState } from "vuex"
import Identicon from "components/identicon"
export default {
computed: mapState({
wallets: state => state.gateway.wallets,
status: state => state.gateway.wallet.status
}),
methods: {
openWallet(wallet) {
if(wallet.password_protected !== false) {
this.$q.dialog({
title: "Password",
message: "Enter wallet password to continue.",
prompt: {
model: "",
type: "password"
},
ok: {
label: "OPEN"
},
cancel: {
flat: true,
label: "CANCEL"
}
}).then(password => {
this.$q.loading.show({
delay: 0
})
this.$gateway.send("wallet", "open_wallet", {name: wallet.name, password: password});
})
} 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" });
},
importWallet() {
this.$router.replace({ path: "wallet-select/import" });
},
importLegacyWallet() {
this.$router.replace({ path: "wallet-select/import-legacy" });
}
},
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
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
}
}
</script>
<style>
</style>

View File

@ -0,0 +1,156 @@
<template>
<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"
/>
</q-field>
<q-field>
<q-input
v-model="wallet.seed"
float-label="Mnemonic seed"
type="textarea"
@blur="$v.wallet.seed.$touch"
:error="$v.wallet.seed.$error"
/>
</q-field>
<q-field>
<q-input v-model="wallet.refresh_start_height" type="number"
min="0" float-label="Restore height"
@blur="$v.wallet.refresh_start_height.$touch"
:error="$v.wallet.refresh_start_height.$error"
/>
</q-field>
<q-field>
<q-input v-model="wallet.password" type="password" float-label="Password" />
</q-field>
<q-field>
<q-input v-model="wallet.password_confirm" type="password" float-label="Confirm Password" />
</q-field>
<q-btn color="primary" @click="restore_wallet" label="Restore wallet" />
</div>
</q-page>
</template>
<script>
import { required, numeric } from "vuelidate/lib/validators"
import { mapState } from "vuex"
export default {
data () {
return {
wallet: {
name: "",
seed: "",
refresh_start_height: 0,
password: "",
password_confirm: ""
},
}
},
computed: mapState({
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: "Enter a wallet name"
})
return
}
if (this.$v.wallet.seed.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: "Enter seed words"
})
return
}
let seed = this.wallet.seed.trim().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: "Invalid seed word length"
})
return
}
if (this.$v.wallet.refresh_start_height.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: "Invalid restore height"
})
return
}
if(this.wallet.password != this.wallet.password_confirm) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: "Passwords do not match"
})
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>

View File

@ -0,0 +1,94 @@
<template>
<q-page>
<div class="row q-pt-sm q-mx-md q-mb-none items-center non-selectable" style="height: 44px;">
<div class="col-8">
<q-icon name="person" size="24px" /> Address book
</div>
<div class="col-4">
</div>
</div>
<template v-if="address_book_starred.length || address_book.length">
<q-list link no-border>
<q-item v-for="(entry, index) in address_book_starred" @click.native="details(entry)">
<q-item-side>
<Identicon :address="entry.address" />
</q-item-side>
<q-item-main>
<q-item-tile class="monospace ellipsis" label>{{ entry.address }}</q-item-tile>
<q-item-tile sublabel>{{ entry.name }}</q-item-tile>
</q-item-main>
<q-item-side class="self-start">
<q-icon size="24px" name="star" />
</q-item-side>
</q-item>
<q-item v-for="(entry, index) in address_book" @click.native="details(entry)">
<q-item-side>
<Identicon :address="entry.address" />
</q-item-side>
<q-item-main>
<q-item-tile class="monospace ellipsis" label>{{ entry.address }}</q-item-tile>
<q-item-tile sublabel>{{ entry.name }}</q-item-tile>
</q-item-main>
<q-item-side class="self-start">
<q-icon size="24px" name="star_border" />
</q-item-side>
</q-item>
</q-list>
</template>
<template v-else>
<p class="q-ma-md">Address book is empty</p>
</template>
<q-page-sticky position="bottom-right" :offset="[18, 18]">
<q-btn
:disable="!is_ready"
round
color="primary"
@click="addEntry"
icon="add"
/>
</q-page-sticky>
<AddressBookDetails ref="addressBookDetails" />
</q-page>
</template>
<script>
import { mapState } from "vuex"
import Identicon from "components/identicon"
import AddressBookDetails from "components/address_book_details"
export default {
computed: mapState({
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"]
}
}),
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
}
},
components: {
Identicon,
AddressBookDetails
}
}
</script>
<style>
</style>

View File

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

View File

@ -0,0 +1,82 @@
<template>
<q-page>
<div class="row q-pt-sm q-mx-md q-mb-none items-center non-selectable" style="height: 44px;">
<div class="col-8">
<q-icon name="call_received" size="24px" /> Receive Ryo
</div>
<div class="col-4">
</div>
</div>
<q-list link no-border>
<q-list-header>My primary address</q-list-header>
<q-item v-for="(address, index) in address_list.primary" @click.native="details(address)">
<q-item-side>
<Identicon :address="address.address" />
</q-item-side>
<q-item-main>
<q-item-tile class="monospace ellipsis" label>{{ address.address }}</q-item-tile>
<q-item-tile sublabel>Primary address</q-item-tile>
</q-item-main>
</q-item>
<template v-if="address_list.used.length">
<q-list-header>My used addresses</q-list-header>
<q-item v-for="(address, index) in address_list.used" @click.native="details(address)">
<q-item-side>
<Identicon :address="address.address" />
</q-item-side>
<q-item-main>
<q-item-tile class="monospace ellipsis" label>{{ address.address }}</q-item-tile>
<q-item-tile sublabel>Sub-address (Index {{ address.address_index }})</q-item-tile>
</q-item-main>
</q-item>
</template>
<template v-if="address_list.unused.length">
<q-list-header>My unused addresses</q-list-header>
<q-item v-for="(address, index) in address_list.unused" @click.native="details(address)">
<q-item-side>
<Identicon :address="address.address" />
</q-item-side>
<q-item-main>
<q-item-tile class="monospace ellipsis" label>{{ address.address }}</q-item-tile>
<q-item-tile sublabel>Sub-address (Index {{ address.address_index }})</q-item-tile>
</q-item-main>
</q-item>
</template>
</q-list>
<AddressDetails ref="addressDetails" />
</q-page>
</template>
<style>
</style>
<script>
import { mapState } from "vuex"
import Identicon from "components/identicon"
import AddressDetails from "components/address_details"
export default {
computed: mapState({
address_list: state => state.gateway.wallet.address_list
}),
methods: {
details (address) {
this.$refs.addressDetails.address = address;
this.$refs.addressDetails.isVisible = true;
}
},
components: {
Identicon,
AddressDetails,
}
}
</script>

282
src/pages/wallet/send.vue Normal file
View File

@ -0,0 +1,282 @@
<template>
<q-page>
<div class="row q-pt-sm q-mx-md q-mb-none items-center non-selectable" style="height: 44px;">
<div class="col-8">
<q-icon name="call_made" size="24px" /> Send Ryo
</div>
<div class="col-4">
</div>
</div>
<div class="q-pa-md">
<div class="row items-end gutter-md">
<div class="col">
<q-field class="q-ma-none">
<q-input v-model="newTx.amount" float-label="Amount"
type="number" min="0" :max="unlocked_balance / 1e9" />
</q-field>
</div>
<div>
<q-btn @click="newTx.amount = unlocked_balance / 1e9">All coins</q-btn>
</div>
</div>
<q-item class="q-pa-none">
<q-item-side>
<Identicon :address="newTx.address" />
</q-item-side>
<q-item-main>
<q-field>
<q-input v-model="newTx.address" float-label="Address"
@blur="$v.newTx.address.$touch"
:error="$v.newTx.address.$error"
/>
</q-field>
</q-item-main>
</q-item>
<q-field style="margin-top:0">
<q-input v-model="newTx.payment_id" float-label="Payment ID (optional)"
@blur="$v.newTx.payment_id.$touch"
:error="$v.newTx.payment_id.$error"
/>
</q-field>
<div class="row gutter-md">
<div class="col-6">
<q-field>
<q-select
v-model="newTx.mixin"
float-label="Mixin"
:options="mixinOptions"
/>
</q-field>
</div>
<div class="col-6">
<q-field>
<q-select
v-model="newTx.priority"
float-label="Priority"
:options="priorityOptions"
/>
</q-field>
</div>
</div>
<q-field>
<q-checkbox v-model="newTx.address_book.save" label="Save to address book" />
</q-field>
<div v-if="newTx.address_book.save">
<q-field>
<q-input v-model="newTx.address_book.name" float-label="Name" />
</q-field>
<q-field>
<q-input v-model="newTx.address_book.description" type="textarea" rows="2" float-label="Notes" />
</q-field>
</div>
<q-field class="q-pt-sm">
<q-btn
:disable="!is_ready"
color="primary" @click="send()" label="Send" />
</q-field>
</div>
<q-inner-loading :visible="tx_status.sending">
<q-spinner color="primary" :size="30" />
</q-inner-loading>
</q-page>
</template>
<script>
import { mapState } from "vuex"
import { required, decimal } from "vuelidate/lib/validators"
import { payment_id, address } from "src/validators/common"
import Identicon from "components/identicon"
const objectAssignDeep = require("object-assign-deep");
export default {
computed: mapState({
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"]
}
}),
data () {
return {
sending: false,
newTx: {
amount: 0,
address: "",
payment_id: "",
mixin: 12,
priority: 0,
address_book: {
save: false,
name: "",
description: ""
}
},
mixinOptions: [
{label: "12 mixins (default)", value: 12},
{label: "48 mixins (top secret)", value: 48},
{label: "96 mixins (paranoid)", value: 60},
],
priorityOptions: [
{label: "Normal (x1 fee)", value: 0},
{label: "High (x2 fee)", value: 1},
{label: "High (x4 fee)", value: 2},
{label: "High (x20 fee)", value: 3},
{label: "Highest (x144 fee)", value: 4},
],
}
},
validations: {
newTx: {
amount: {
required,
decimal
},
address: { required, address },
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: "",
mixin: 12,
priority: 0,
address_book: {
save: false,
name: "",
description: ""
}
}
break;
case -1:
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.tx_status.message
})
break;
}
},
deep: true
}
},
methods: {
send: function () {
this.$v.newTx.$touch()
if(this.newTx.amount < 0) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: "Amount cannot be negative"
})
return
} else if(this.newTx.amount == 0) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: "Amount must be greater than zero"
})
return
} else if(this.newTx.amount > this.unlocked_balance / 1e9) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: "Not enough unlocked balance"
})
return
} else if (this.$v.newTx.amount.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: "Amount not valid"
})
return
}
if (this.$v.newTx.address.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: "Address not valid"
})
return
}
if (this.$v.newTx.payment_id.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: "Payment id not valid"
})
return
}
this.$q.dialog({
title: "Transfer",
message: "Enter wallet password to continue.",
prompt: {
model: "",
type: "password"
},
ok: {
label: "SEND"
},
cancel: {
flat: true,
label: "CANCEL"
}
}).then(password => {
this.$store.commit("gateway/set_tx_status", {
code: 1,
message: "Sending transaction",
sending: true
})
let newTx = objectAssignDeep.noMutate(this.newTx, {password})
this.$gateway.send("wallet", "transfer", newTx)
})
}
},
components: {
Identicon
}
}
</script>
<style>
</style>

View File

@ -0,0 +1,53 @@
<template>
<q-page>
<div class="row q-pt-sm q-mx-md q-mb-sm items-center non-selectable">
<div class="col-8">
<q-icon name="history" size="24px" /> Transaction history
</div>
<div class="col-4">
<q-select
v-model="tx_type"
float-label="Filter by transaction type"
:options="tx_type_options"
/>
</div>
</div>
<TxList :type="tx_type" />
</q-page>
</template>
<script>
import { mapState } from "vuex"
import TxList from "components/tx_list"
export default {
data () {
return {
tx_type: "all",
tx_type_options: [
{label: "All", value: "all"},
{label: "Incoming", value: "in"},
{label: "Outgoing", value: "out"},
{label: "Pending incoming", value: "pool"},
{label: "Pending outgoing", value: "pending"},
{label: "Failed", value: "failed"},
]
}
},
computed: mapState({
tx_list: state => state.gateway.wallet.transactions.tx_list
}),
components: {
TxList
}
}
</script>
<style lang="scss">
</style>

206
src/pages/wallet/wallet.vue Normal file
View File

@ -0,0 +1,206 @@
<template>
<q-page padding>
<AddressHeader :address="info.address" :header="info.name" :subheader="info.address" />
<div class="row justify-between" style="max-width: 768px">
<div class="infoBox">
<div class="infoBoxContent">
<div class="text"><span>Balance</span></div>
<div class="value"><span><FormatRyo :amount="info.balance" /></span></div>
</div>
</div>
<div class="infoBox">
<div class="infoBoxContent">
<div class="text"><span>Unlocked balance</span></div>
<div class="value"><span><FormatRyo :amount="info.unlocked_balance" /></span></div>
</div>
</div>
<div class="infoBox q-pt-md">
<q-btn
:disable="!is_ready"
flat @click="getPrivateKeys()">Show seed words</q-btn>
<q-btn
:disable="!is_ready"
flat @click="rescan_modal_show = true">Rescan Wallet</q-btn>
</div>
</div>
<div>
<h6 class="q-my-none">Recent transactions:</h6>
<TxList :limit="5" />
</div>
<q-inner-loading :visible="spinner">
<q-spinner color="primary" :size="30" />
</q-inner-loading>
<q-modal minimized v-model="private_keys_modal_show" @hide="closePrivateKeys()">
<div class="q-ma-md">
<h6 class="q-mb-xs q-mt-lg">Seed words</h6>
<p>{{ secret.mnemonic }}</p>
<template v-if="secret.view_key != secret.spend_key">
<h6 class="q-mb-xs">View key</h6>
<p>{{ secret.view_key }}</p>
</template>
<h6 class="q-mb-xs">Spend key</h6>
<p>{{ secret.spend_key }}</p>
<q-btn
color="primary"
@click="private_keys_modal_show = false"
label="Close"
/>
</div>
</q-modal>
<q-modal minimized v-model="rescan_modal_show">
<div class="q-ma-md">
<h4 class="q-mt-lg q-mb-md">Rescan wallet</h4>
<p>Select full rescan or rescan of spent outputs only.</p>
<div class="q-mt-lg">
<q-radio v-model="rescan_type" val="full" label="Rescan full blockchain" />
</div>
<div class="q-mt-sm">
<q-radio v-model="rescan_type" val="spent" label="Rescan spent outputs" />
</div>
<div class="q-mt-xl text-right">
<q-btn
flat class="q-mr-sm"
@click="rescan_modal_show = false"
label="Close"
/>
<q-btn
color="primary"
@click="rescanWallet()"
label="Rescan"
/>
</div>
</div>
</q-modal>
</q-page>
</template>
<script>
import { mapState } from "vuex"
import AddressHeader from "components/address_header"
import FormatRyo from "components/format_ryo"
import TxList from "components/tx_list"
export default {
computed: mapState({
info: state => state.gateway.wallet.info,
secret: state => state.gateway.wallet.secret,
is_ready (state) {
return this.$store.getters["gateway/isReady"]
}
}),
data () {
return {
spinner: false,
private_keys_modal_show: false,
rescan_modal_show: false,
rescan_type: "full"
}
},
watch: {
secret: {
handler(val, old) {
if(val.view_key == old.view_key) return
this.spinner = false
console.log(this.secret.view_key)
switch(this.secret.view_key) {
case "":
break
case -1:
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.secret.mnemonic
})
this.$store.commit("gateway/set_wallet_data", {
secret: {
mnemonic: "",
spend_key: "",
view_key: ""
}
})
break
default:
this.private_keys_modal_show = true
break
}
},
deep: true
}
},
methods: {
getPrivateKeys () {
this.$q.dialog({
title: "Show seed words",
message: "Enter wallet password to continue.",
prompt: {
model: "",
type: "password"
},
ok: {
label: "SHOW"
},
cancel: {
flat: true,
label: "CANCEL"
}
}).then(password => {
//this.spinner = true
this.$gateway.send("wallet", "get_private_keys", {password})
})
},
closePrivateKeys () {
this.private_keys_modal_show = false
setTimeout(() => {
this.$store.commit("gateway/set_wallet_data", {
secret: {
mnemonic: "",
spend_key: "",
view_key: ""
}
})
}, 500)
},
rescanWallet () {
this.rescan_modal_show = false
if(this.rescan_type == "full") {
this.$gateway.send("wallet", "rescan_blockchain")
} else {
this.$gateway.send("wallet", "rescan_spent")
}
}
},
components: {
FormatRyo,
AddressHeader,
TxList
},
}
</script>
<style lang="scss">
.layout-padding {
padding: 15px 16px !important;
}
</style>

0
src/plugins/.gitkeep Normal file
View File

7
src/plugins/axios.js Normal file
View File

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

19
src/plugins/gateway.js Normal file
View File

@ -0,0 +1,19 @@
import { Gateway } from "src/gateway/gateway"
/* This plugin gets called early in the life-cycle
In the future, we can detect what platform we
are on and include the correct gateway.
The gateway just gets stored into the app global
object to be called from anywhere within the
frontend
*/
export default ({
app,
router,
store,
Vue
}) => {
Vue.prototype.$gateway = new Gateway(app, router)
}

16
src/plugins/i18n.js Normal file
View File

@ -0,0 +1,16 @@
import VueI18n from "vue-i18n"
import messages from "src/i18n"
export default ({
app,
Vue
}) => {
Vue.use(VueI18n)
// Set i18n instance on app
app.i18n = new VueI18n({
locale: "en-us",
fallbackLocale: "en-us",
messages
})
}

12
src/plugins/timeago.js Normal file
View File

@ -0,0 +1,12 @@
import VueTimeago from 'vue-timeago'
export default ({
app,
router,
store,
Vue
}) => {
Vue.use(VueTimeago, {
name: 'Timeago',
locale: 'en'
})
}

5
src/plugins/vuelidate.js Normal file
View File

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

26
src/router/index.js Normal file
View File

@ -0,0 +1,26 @@
import Vue from "vue"
import VueRouter from "vue-router"
import routes from "./routes"
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 ""
*/
// 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

110
src/router/routes.js Normal file
View File

@ -0,0 +1,110 @@
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",
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: "/wallet",
component: () =>
import ("layouts/wallet/main"),
children: [
{
path: "",
component: () =>
import ("pages/wallet/wallet")
},
{
path: "receive",
component: () =>
import ("pages/wallet/receive")
},
{
path: "send",
component: () =>
import ("pages/wallet/send")
},
{
path: "addressbook",
component: () =>
import ("pages/wallet/addressbook")
},
{
path: "txhistory",
component: () =>
import ("pages/wallet/txhistory")
},
]
},
{ // Always leave this as last one
path: "*",
component: () =>
import ("pages/404")
}
]

View File

@ -0,0 +1,18 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 859.4011 116.8585">
<defs>
<linearGradient id="a" x2="0" y1="168.1192" y2="286.5673" gradientTransform="translate(82.8992 -169.0231)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#62c9f3"/>
<stop offset="1" stop-color="#3d58b0"/>
</linearGradient>
<linearGradient id="c" x2="0" y1="168.1192" y2="286.5673" gradientTransform="translate(82.8992 -169.0231)" gradientUnits="userSpaceOnUse" xlink:href="#a"/>
<linearGradient id="e" x1="138.7479" x2="138.7479" y1="10.1293" y2="104.051" gradientTransform="translate(0 183.1415)" gradientUnits="userSpaceOnUse" xlink:href="#a"/>
<linearGradient id="f" x1="138.7479" x2="138.7479" y1="10.1293" y2="104.051" gradientTransform="translate(0 183.1415)" gradientUnits="userSpaceOnUse" xlink:href="#a"/>
<linearGradient id="g" x1="138.7479" x2="138.7479" y1="10.1293" y2="104.051" gradientTransform="translate(0 183.1415)" gradientUnits="userSpaceOnUse" xlink:href="#a"/>
</defs>
<path fill="url(#c)" d="M41.7535 41.89H75.105v33.0785H41.7535zm-4.2333-4.2334v41.5453h41.8181V37.6565zM58.4295 4.2333c29.9566 0 54.1957 24.2396 54.1957 54.1962 0 29.9566-24.239 54.1957-54.1957 54.1957-29.9566 0-54.1962-24.239-54.1962-54.1957 0-29.9566 24.2396-54.1962 54.1962-54.1962zm0-4.2333C26.185 0 0 26.185 0 58.4295s26.185 58.429 58.4295 58.429 58.429-26.1845 58.429-58.429S90.674 0 58.4295 0z"/>
<g transform="translate(12 -183.1415)">
<path fill="url(#e)" d="M191.1345 287.4374l-15.7334-39.4667q-1.7333.2667-5.4666.2667h-34v39.2h-4.8v-91.7333h40.8q10.5333 0 15.7333 3.6 5.2 3.6 6.5333 9.0666 1.3334 5.3334 1.3334 13.6 0 6.6667-.9334 11.4667-.9333 4.6667-4.2666 8.4-3.3334 3.7333-10 5.3333l16.1333 40.2667zm-21.4667-43.7333q9.3334 0 13.8667-2.6667 4.5333-2.6667 5.8666-7.0667 1.3334-4.5333 1.3334-12 0-7.6-1.2-12-1.0667-4.4-5.2-7.0666-4-2.6667-12.6667-2.6667h-35.7333v43.4667zm63.6625 43.7333v-37.4667l-34.8-54.2666h5.6l31.2 49.2h.9333l31.3334-49.2h5.4666l-34.9333 54.2666v37.4667zm79.7979 1.0666q-17.8666 0-25.0666-3.0666-7.2-3.0667-9.3334-12.1333-2-9.0667-2-31.7334 0-22.6666 2-31.7333 2.1334-9.0666 9.3334-12.1333 7.2-3.0667 25.0666-3.0667 17.8667 0 25.0667 3.0667 7.2 3.0666 9.2 12.1333 2.1333 9.0667 2.1333 31.7333 0 22.6667-2.1333 31.7334-2 9.0666-9.2 12.1333-7.2 3.0667-25.0667 3.0667zm0-4.5333q16.1334 0 21.8667-2 5.8667-2.1333 7.7333-10.2666 2-8.2667 2-30.1334 0-21.8666-2-30-1.8666-8.2666-7.7333-10.2666-5.7333-2.1334-21.8667-2.1334-16.1333 0-22 2.1334-5.7333 2-7.7333 10.2666-1.8667 8.1334-1.8667 30 0 21.8667 1.8667 30.1334 2 8.1333 7.7333 10.2666 5.8667 2 22 2zm153.3563 3.4667l-27.0667-83.0667h-.5333l-27.0667 83.0667h-4.6666l-27.2-91.7333h5.2l24.1333 83.0666h.8l26.6667-83.0666h5.0666l26.6667 83.0666h.8l24.1333-83.0666h4.9333l-27.2 91.7333zm104.6416 0l-12.1333-29.6h-45.8667l-12.1333 29.6h-5.0667l37.8667-91.7333h4.6667l37.8666 91.7333zm-34.6666-84.9333h-.6667l-20.8 50.8h42.1333zm51.6125 84.9333v-91.7333h4.8v87.2h46.6666v4.5333zm62.3541 0v-91.7333h4.8v87.2h46.6667v4.5333z"/>
<path fill="url(#f)" d="M712.7803 287.4374v-91.7333h56.8v4.5333h-52v38.5333h45.7333v4.5334h-45.7333v39.6h52v4.5333z"/>
<path fill="url(#g)" d="M811.5345 287.4374v-87.2h-31.0667v-4.5333h66.9333v4.5333h-31.0666v87.2z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

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

View File

@ -0,0 +1,16 @@
export const isReady = (state) => {
let target_height
if(state.app.config.daemon_type === "local" && !state.daemon.info.is_ready)
target_height = Math.max(state.daemon.info.height, state.daemon.info.target_height)
else
target_height = state.daemon.info.height
if(state.app.config.daemon_type === "local") {
return state.daemon.info.is_ready && state.wallet.info.height >= target_height - 1
} else {
return state.wallet.info.height >= target_height - 1
}
return false
}

View File

@ -0,0 +1,12 @@
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
}

View File

@ -0,0 +1,17 @@
const objectAssignDeep = require("object-assign-deep");
export const set_app_data = (state, data) => {
state.app = objectAssignDeep.noMutate(state.app, data)
}
export const set_daemon_data = (state, data) => {
state.daemon = objectAssignDeep.noMutate(state.daemon, data)
}
export const set_wallet_data = (state, data) => {
state.wallet = objectAssignDeep.noMutate(state.wallet, data)
}
export const set_wallet_list = (state, data) => {
state.wallets = objectAssignDeep.noMutate(state.wallets, data)
}
export const set_tx_status = (state, data) => {
state.tx_status = data
}

View File

@ -0,0 +1,69 @@
export default {
app: {
status: {
code: 1 // Connecting to backend
},
config: {
},
pending_config: {
}
},
wallets: {
list: [],
legacy: []
},
wallet: {
status: {
code: 1,
message: null
},
info: {
name: "",
address: "",
height: 0,
balance: 0,
unlocked_balance: 0,
},
secret: {
mnemonic: "",
view_key: "",
spend_key: ""
},
transactions: {
tx_list: [],
},
address_list: {
used: [],
unused: [],
address_book: [],
}
},
tx_status: {
code: 0,
message: ""
},
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: []
}
}

14
src/store/index.js Normal file
View File

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

File diff suppressed because one or more lines are too long

39
src/validators/common.js Normal file
View File

@ -0,0 +1,39 @@
//import { validateAddress } from "./address_tools"
export const payment_id = (input) => {
return input.length === 0 || (/^[0-9A-Fa-f]+$/.test(input) && (input.length == 16 || input.length == 64))
}
export const address = (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
}
}