├── babel.config.js ├── public ├── icon.png ├── favicon.png ├── icon128.png ├── icon16.png ├── icon48.png ├── thumbnail.png ├── thumbnail.psd ├── css │ ├── iconfont.eot │ ├── iconfont.ttf │ ├── iconfont.woff │ └── iconfont.woff2 ├── index.html └── logo.svg ├── .postcssrc.js ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── ISSUE_TEMPLATE.md ├── workflows │ ├── master.yml │ └── staging.yml ├── CONTRIBUTING.md └── CODE_OF_CONDUCT.md ├── .prettierrc.js ├── vue.config.js ├── src ├── assets │ ├── fonts │ │ ├── lineto-circular-bold.ttf │ │ ├── lineto-circular-book.ttf │ │ ├── lineto-circular-black.eot │ │ ├── lineto-circular-black.ttf │ │ ├── lineto-circular-black.woff │ │ ├── lineto-circular-medium.ttf │ │ ├── lineto-circular-pro-bold.eot │ │ ├── lineto-circular-pro-bold.ttf │ │ ├── lineto-circular-pro-bold.woff │ │ ├── lineto-circular-pro-book.eot │ │ ├── lineto-circular-pro-book.ttf │ │ ├── lineto-circular-pro-book.woff │ │ ├── lineto-circular-blackItalic.ttf │ │ ├── lineto-circular-boldItalic.ttf │ │ ├── lineto-circular-bookItalic.ttf │ │ ├── lineto-circular-mediumItalic.ttf │ │ ├── lineto-circular-pro-medium.eot │ │ ├── lineto-circular-pro-medium.ttf │ │ ├── lineto-circular-pro-medium.woff │ │ ├── lineto-circular-pro-bookItalic.eot │ │ ├── lineto-circular-pro-bookItalic.ttf │ │ └── lineto-circular-pro-bookItalic.woff │ └── img │ │ ├── bg.svg │ │ └── shapes.svg ├── persisted.js ├── components │ ├── Operation │ │ ├── ValueBool.vue │ │ ├── ValueJson.vue │ │ ├── ValueAccount.vue │ │ ├── Header.vue │ │ ├── ValueAmount.vue │ │ └── Value.vue │ ├── Center.vue │ ├── App.vue │ ├── Confirmation.vue │ ├── Error.vue │ ├── Operation.vue │ ├── Footer.vue │ ├── Search.vue │ ├── Avatar.vue │ ├── OpenExternal.vue │ ├── Header.vue │ └── Modal │ │ └── Profile.vue ├── number.json ├── translation.json ├── store │ ├── index.js │ └── modules │ │ ├── ui.js │ │ ├── index.js │ │ ├── persistentForms.js │ │ ├── settings.js │ │ └── auth.js ├── views │ ├── 404.vue │ ├── Dashboard.vue │ ├── Home.vue │ ├── About.vue │ ├── Accounts.vue │ ├── Auths.vue │ ├── Settings.vue │ ├── Apps.vue │ ├── Revoke.vue │ ├── Authorize.vue │ ├── Sign.vue │ ├── LoginRequest.vue │ ├── Developers.vue │ ├── Profile.vue │ ├── Login.vue │ └── Import.vue ├── vars.less ├── helpers │ ├── messages.json │ ├── keychain.js │ ├── idle.js │ ├── client.js │ ├── auth.js │ ├── utils.js │ └── operations.json ├── App.vue ├── main.js ├── fonts.less ├── router.js └── styles.less ├── tests └── unit │ └── example.spec.js ├── .editorconfig ├── .gitignore ├── start.js ├── .eslintrc.js ├── jest.config.js ├── LICENSE ├── README.md └── package.json /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@vue/app'], 3 | }; 4 | -------------------------------------------------------------------------------- /public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ledgerconnect/hivesigner/HEAD/public/icon.png -------------------------------------------------------------------------------- /.postcssrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {}, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ledgerconnect/hivesigner/HEAD/public/favicon.png -------------------------------------------------------------------------------- /public/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ledgerconnect/hivesigner/HEAD/public/icon128.png -------------------------------------------------------------------------------- /public/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ledgerconnect/hivesigner/HEAD/public/icon16.png -------------------------------------------------------------------------------- /public/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ledgerconnect/hivesigner/HEAD/public/icon48.png -------------------------------------------------------------------------------- /public/thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ledgerconnect/hivesigner/HEAD/public/thumbnail.png -------------------------------------------------------------------------------- /public/thumbnail.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ledgerconnect/hivesigner/HEAD/public/thumbnail.psd -------------------------------------------------------------------------------- /public/css/iconfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ledgerconnect/hivesigner/HEAD/public/css/iconfont.eot -------------------------------------------------------------------------------- /public/css/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ledgerconnect/hivesigner/HEAD/public/css/iconfont.ttf -------------------------------------------------------------------------------- /public/css/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ledgerconnect/hivesigner/HEAD/public/css/iconfont.woff -------------------------------------------------------------------------------- /public/css/iconfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ledgerconnect/hivesigner/HEAD/public/css/iconfont.woff2 -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Fixes # . 2 | 3 | Changes proposed in this pull request: 4 | - 5 | - 6 | - 7 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | trailingComma: 'all', 4 | printWidth: 100, 5 | }; 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Expected behavior 2 | 3 | ### Actual behavior 4 | 5 | ### Steps to reproduce the behavior 6 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | outputDir: path.resolve(__dirname, './www'), 5 | }; 6 | -------------------------------------------------------------------------------- /src/assets/fonts/lineto-circular-bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ledgerconnect/hivesigner/HEAD/src/assets/fonts/lineto-circular-bold.ttf -------------------------------------------------------------------------------- /src/assets/fonts/lineto-circular-book.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ledgerconnect/hivesigner/HEAD/src/assets/fonts/lineto-circular-book.ttf -------------------------------------------------------------------------------- /tests/unit/example.spec.js: -------------------------------------------------------------------------------- 1 | describe('example', () => { 2 | it('should work', () => { 3 | expect(true).toBe(true); 4 | }); 5 | }); 6 | -------------------------------------------------------------------------------- /src/assets/fonts/lineto-circular-black.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ledgerconnect/hivesigner/HEAD/src/assets/fonts/lineto-circular-black.eot -------------------------------------------------------------------------------- /src/assets/fonts/lineto-circular-black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ledgerconnect/hivesigner/HEAD/src/assets/fonts/lineto-circular-black.ttf -------------------------------------------------------------------------------- /src/assets/fonts/lineto-circular-black.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ledgerconnect/hivesigner/HEAD/src/assets/fonts/lineto-circular-black.woff -------------------------------------------------------------------------------- /src/assets/fonts/lineto-circular-medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ledgerconnect/hivesigner/HEAD/src/assets/fonts/lineto-circular-medium.ttf -------------------------------------------------------------------------------- /src/assets/fonts/lineto-circular-pro-bold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ledgerconnect/hivesigner/HEAD/src/assets/fonts/lineto-circular-pro-bold.eot -------------------------------------------------------------------------------- /src/assets/fonts/lineto-circular-pro-bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ledgerconnect/hivesigner/HEAD/src/assets/fonts/lineto-circular-pro-bold.ttf -------------------------------------------------------------------------------- /src/assets/fonts/lineto-circular-pro-bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ledgerconnect/hivesigner/HEAD/src/assets/fonts/lineto-circular-pro-bold.woff -------------------------------------------------------------------------------- /src/assets/fonts/lineto-circular-pro-book.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ledgerconnect/hivesigner/HEAD/src/assets/fonts/lineto-circular-pro-book.eot -------------------------------------------------------------------------------- /src/assets/fonts/lineto-circular-pro-book.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ledgerconnect/hivesigner/HEAD/src/assets/fonts/lineto-circular-pro-book.ttf -------------------------------------------------------------------------------- /src/assets/fonts/lineto-circular-pro-book.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ledgerconnect/hivesigner/HEAD/src/assets/fonts/lineto-circular-pro-book.woff -------------------------------------------------------------------------------- /src/persisted.js: -------------------------------------------------------------------------------- 1 | import store from '@/store'; 2 | 3 | export default function getPersistedData(callback) { 4 | return callback({ store }); 5 | } 6 | -------------------------------------------------------------------------------- /src/assets/fonts/lineto-circular-blackItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ledgerconnect/hivesigner/HEAD/src/assets/fonts/lineto-circular-blackItalic.ttf -------------------------------------------------------------------------------- /src/assets/fonts/lineto-circular-boldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ledgerconnect/hivesigner/HEAD/src/assets/fonts/lineto-circular-boldItalic.ttf -------------------------------------------------------------------------------- /src/assets/fonts/lineto-circular-bookItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ledgerconnect/hivesigner/HEAD/src/assets/fonts/lineto-circular-bookItalic.ttf -------------------------------------------------------------------------------- /src/assets/fonts/lineto-circular-mediumItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ledgerconnect/hivesigner/HEAD/src/assets/fonts/lineto-circular-mediumItalic.ttf -------------------------------------------------------------------------------- /src/assets/fonts/lineto-circular-pro-medium.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ledgerconnect/hivesigner/HEAD/src/assets/fonts/lineto-circular-pro-medium.eot -------------------------------------------------------------------------------- /src/assets/fonts/lineto-circular-pro-medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ledgerconnect/hivesigner/HEAD/src/assets/fonts/lineto-circular-pro-medium.ttf -------------------------------------------------------------------------------- /src/assets/fonts/lineto-circular-pro-medium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ledgerconnect/hivesigner/HEAD/src/assets/fonts/lineto-circular-pro-medium.woff -------------------------------------------------------------------------------- /src/assets/fonts/lineto-circular-pro-bookItalic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ledgerconnect/hivesigner/HEAD/src/assets/fonts/lineto-circular-pro-bookItalic.eot -------------------------------------------------------------------------------- /src/assets/fonts/lineto-circular-pro-bookItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ledgerconnect/hivesigner/HEAD/src/assets/fonts/lineto-circular-pro-bookItalic.ttf -------------------------------------------------------------------------------- /src/assets/fonts/lineto-circular-pro-bookItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ledgerconnect/hivesigner/HEAD/src/assets/fonts/lineto-circular-pro-bookItalic.woff -------------------------------------------------------------------------------- /src/components/Operation/ValueBool.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = LF 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /src/number.json: -------------------------------------------------------------------------------- 1 | { 2 | "en": { 3 | "currency": { 4 | "style": "currency", 5 | "currency": "USD" 6 | }, 7 | "percent": { 8 | "style": "percent" 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "en": { 3 | "message": { 4 | "hello": "hello world" 5 | } 6 | }, 7 | "fr": { 8 | "message": { 9 | "hello": "こんにちは、世界" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/components/Center.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /src/components/Operation/ValueJson.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuex from 'vuex'; 3 | import modules from './modules'; 4 | 5 | Vue.use(Vuex); 6 | 7 | const store = new Vuex.Store({ 8 | modules, 9 | strict: process.env.NODE_ENV !== 'production', 10 | }); 11 | 12 | export default store; 13 | -------------------------------------------------------------------------------- /src/views/404.vue: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | dist 4 | www 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | 15 | # Editor directories and files 16 | .idea 17 | .vscode 18 | *.suo 19 | *.ntvs* 20 | *.njsproj 21 | *.sln 22 | *.sw* 23 | 24 | package-lock.json -------------------------------------------------------------------------------- /src/components/Operation/ValueAccount.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 14 | -------------------------------------------------------------------------------- /src/components/App.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | -------------------------------------------------------------------------------- /src/components/Confirmation.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 15 | -------------------------------------------------------------------------------- /src/components/Operation/Header.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 19 | -------------------------------------------------------------------------------- /src/store/modules/ui.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | 3 | const state = { 4 | savedPath: null, 5 | }; 6 | 7 | const mutations = { 8 | savePath(_state, path) { 9 | Vue.set(_state, 'savedPath', path); 10 | }, 11 | }; 12 | 13 | const actions = { 14 | savePath({ commit }, path) { 15 | commit('savePath', path); 16 | }, 17 | }; 18 | 19 | export default { 20 | state, 21 | mutations, 22 | actions, 23 | }; 24 | -------------------------------------------------------------------------------- /src/store/modules/index.js: -------------------------------------------------------------------------------- 1 | import { camelCase } from 'lodash'; 2 | 3 | const requireModule = require.context('.', false, /\.js$/); 4 | const modules = {}; 5 | 6 | requireModule.keys().forEach(fileName => { 7 | if (fileName === './index.js') return; 8 | const moduleName = camelCase(fileName.replace(/(\.\/|\.js)/g, '')); 9 | modules[moduleName] = requireModule(fileName).default; 10 | }); 11 | 12 | export default modules; 13 | -------------------------------------------------------------------------------- /src/assets/img/bg.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /start.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const frameguard = require('frameguard'); 3 | const serveStatic = require('serve-static'); 4 | 5 | const app = express(); 6 | app.use(frameguard({ action: 'deny' })); 7 | app.use(serveStatic(`${__dirname}/www`)); 8 | 9 | app.get('*', (req, res) => { 10 | res.sendFile(`${__dirname}/www/index.html`); 11 | }); 12 | 13 | const port = process.env.PORT || 5000; 14 | app.listen(port, () => { 15 | console.log(`Listening on port ${port}`); 16 | }); 17 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | jest: true, 6 | }, 7 | extends: ['plugin:vue/essential', '@vue/airbnb', 'prettier'], 8 | plugins: ['prettier'], 9 | rules: { 10 | 'prettier/prettier': 'error', 11 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 12 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 13 | }, 14 | parserOptions: { 15 | parser: 'babel-eslint', 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /src/vars.less: -------------------------------------------------------------------------------- 1 | @bp-small: ~'only screen and (min-width: 1012px)'; 2 | 3 | @primary-color: #E31337; 4 | @link-color: black; 5 | @text-color: #676767; 6 | @error-color: #f44336; 7 | @success-color: #00DD9E; 8 | @border-color: #e9e7e7; 9 | @input-border-color: #d1d5da; 10 | @bg-color: white; 11 | @white-darker: #f0f0f8; 12 | @heading-color: #202225; 13 | @heading-color: #414141; 14 | 15 | @header-bg: @bg-color; 16 | @header-height: 56px; 17 | 18 | @extension-width: 360px; 19 | @extension-height: 600px; 20 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: ['js', 'jsx', 'json', 'vue'], 3 | transform: { 4 | '^.+\\.vue$': 'vue-jest', 5 | '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub', 6 | '^.+\\.jsx?$': 'babel-jest', 7 | }, 8 | moduleNameMapper: { 9 | '^@/(.*)$': '/src/$1', 10 | }, 11 | snapshotSerializers: ['jest-serializer-vue'], 12 | testMatch: ['**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'], 13 | testURL: 'http://localhost/', 14 | }; 15 | -------------------------------------------------------------------------------- /src/components/Error.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 24 | -------------------------------------------------------------------------------- /src/helpers/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "ERROR_INVALID_CREDENTIALS": "Invalid username or password. You need to use master, active, posting or memo key to login.", 3 | "ERROR_INVALID_ENCRYPTION_KEY": "Invalid hivesigner password.", 4 | "TOOLTIP_LOGIN_ENCRYPTION_KEY": "This is a custom password you've set to unlock your account for usage. This is not your Hive private key. If you forgot your hivesigner password you can import your account again.", 5 | "TOOLTIP_IMPORT_ENCRYPTION_KEY": "This is a new custom password to encrypt your credentials. This is not your Hive private key." 6 | } 7 | -------------------------------------------------------------------------------- /src/components/Operation/ValueAmount.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 32 | -------------------------------------------------------------------------------- /src/views/Dashboard.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 32 | -------------------------------------------------------------------------------- /src/components/Operation.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 30 | -------------------------------------------------------------------------------- /src/components/Footer.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 35 | -------------------------------------------------------------------------------- /src/components/Search.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 26 | 27 | 44 | -------------------------------------------------------------------------------- /src/components/Avatar.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 26 | 27 | 44 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | hivesigner 14 | 15 | 16 | 17 | 20 |
21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/components/OpenExternal.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 34 | -------------------------------------------------------------------------------- /src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 37 | -------------------------------------------------------------------------------- /src/helpers/keychain.js: -------------------------------------------------------------------------------- 1 | export const KEYCHAIN_LOCALSTORAGE_KEY = 'keychain'; 2 | 3 | export function getKeychain() { 4 | const storedKeychain = localStorage.getItem(KEYCHAIN_LOCALSTORAGE_KEY); 5 | let keychain = {}; 6 | if (storedKeychain) { 7 | try { 8 | keychain = JSON.parse(storedKeychain); 9 | } catch (err) { 10 | console.log("Couldn't parse stored hivesigner data", err); 11 | } 12 | } 13 | return keychain; 14 | } 15 | 16 | export function hasAccounts() { 17 | const keychain = getKeychain(); 18 | return Object.keys(keychain).length !== 0; 19 | } 20 | 21 | export function addToKeychain(username, encryptedPassword) { 22 | const keychain = getKeychain(); 23 | keychain[username] = encryptedPassword; 24 | localStorage.setItem(KEYCHAIN_LOCALSTORAGE_KEY, JSON.stringify(keychain)); 25 | } 26 | 27 | export function removeFromKeychain(username) { 28 | const keychain = getKeychain(); 29 | delete keychain[username]; 30 | localStorage.setItem(KEYCHAIN_LOCALSTORAGE_KEY, JSON.stringify(keychain)); 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Hivesigner 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/helpers/idle.js: -------------------------------------------------------------------------------- 1 | import { debounce } from 'lodash'; 2 | 3 | export default function createIdleDetector() { 4 | let triggerIdle = null; 5 | 6 | return { 7 | start(treshold, callback) { 8 | this.stop(); 9 | 10 | triggerIdle = debounce(callback, treshold); 11 | 12 | window.addEventListener('load', triggerIdle); 13 | window.addEventListener('mousemove', triggerIdle); 14 | window.addEventListener('mousedown', triggerIdle); 15 | window.addEventListener('touchstart', triggerIdle); 16 | window.addEventListener('click', triggerIdle); 17 | window.addEventListener('keypress', triggerIdle); 18 | 19 | triggerIdle(); 20 | }, 21 | stop() { 22 | if (!triggerIdle) return; 23 | 24 | window.removeEventListener('load', triggerIdle); 25 | window.removeEventListener('mousemove', triggerIdle); 26 | window.removeEventListener('mousedown', triggerIdle); 27 | window.removeEventListener('touchstart', triggerIdle); 28 | window.removeEventListener('click', triggerIdle); 29 | window.removeEventListener('keypress', triggerIdle); 30 | 31 | triggerIdle.cancel(); 32 | triggerIdle = null; 33 | }, 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /src/components/Header.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 21 | 22 | 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Master CI/CD](https://github.com/ledgerconnect/hivesigner/workflows/Master%20CI/CD/badge.svg?branch=master) 2 | [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/ledgerconnect/hivesigner/master/LICENSE) 3 | [![Discord](https://img.shields.io/discord/352140630769664009.svg?color=%236b80c4&label=discord)](https://discord.gg/pNJn7wh) 4 | 5 | # hivesigner 6 | 7 | > Signer app for Hive 8 | 9 | This is UI component of the Hivesigner. There are [SDK](https://github.com/ledgerconnect/hivesigner.js) and [API](https://github.com/ledgerconnect/hivesigner-api) components, check [Wiki](https://github.com/ledgerconnect/hivesigner/wiki) to learn more. 10 | 11 | ## Usage 12 | 13 | ``` bash 14 | # Install dependencies 15 | npm install 16 | 17 | # Serve on localhost:8080 18 | npm run serve 19 | 20 | # Build for production 21 | npm run build 22 | 23 | # Wrap browser extension 24 | npm run zip 25 | ``` 26 | 27 | ## Issues 28 | 29 | To report a non-critical issue, please file an issue on this GitHub project. 30 | If you find a security issue please report details to `security@hivesigner.com` or trusted community members. 31 | We will evaluate the risk and make a patch available before filing the issue. 32 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 46 | 47 | 56 | -------------------------------------------------------------------------------- /src/store/modules/persistentForms.js: -------------------------------------------------------------------------------- 1 | // Rationale behind those persistent forms is exactly the same as Metamask's: 2 | // https://github.com/MetaMask/metamask-extension/blob/develop/docs/form_persisting_architecture.md 3 | // The only difference is in the implementation (Metamask uses localStorage), in this case we 4 | // save form state to Vuex which is synced to the background script. 5 | import Vue from 'vue'; 6 | 7 | const state = { 8 | login: { 9 | username: '', 10 | key: '', 11 | }, 12 | import: { 13 | step: 1, 14 | username: '', 15 | password: '', 16 | key: '', 17 | keyConfirmation: '', 18 | }, 19 | }; 20 | 21 | const mutations = { 22 | saveLoginUsername(_state, username) { 23 | Vue.set(_state.login, 'username', username); 24 | }, 25 | saveLoginKey(_state, key) { 26 | Vue.set(_state.login, 'key', key); 27 | }, 28 | saveImportStep(_state, step) { 29 | Vue.set(_state.import, 'step', step); 30 | }, 31 | saveImportUsername(_state, username) { 32 | Vue.set(_state.import, 'username', username); 33 | }, 34 | saveImportPassword(_state, password) { 35 | Vue.set(_state.import, 'password', password); 36 | }, 37 | saveImportKey(_state, key) { 38 | Vue.set(_state.import, 'key', key); 39 | }, 40 | saveImportKeyConfirmation(_state, keyConfirmation) { 41 | Vue.set(_state.import, 'keyConfirmation', keyConfirmation); 42 | }, 43 | }; 44 | 45 | const actions = {}; 46 | 47 | export default { state, mutations, actions }; 48 | -------------------------------------------------------------------------------- /src/helpers/client.js: -------------------------------------------------------------------------------- 1 | import { Client } from '@hiveio/dhive'; 2 | import * as hiveuri from 'hive-uri'; 3 | 4 | const CLIENT_OPTIONS = { 5 | timeout: 3000, 6 | failoverThreshold: 15, 7 | consoleOnFailover: true, 8 | }; 9 | const EXPIRE_TIME = 1000 * 60; 10 | 11 | const DEFAULT_SERVER = [ 12 | 'https://rpc.ecency.com', 13 | 'https://api.hive.blog', 14 | 'https://api.deathwing.me', 15 | ]; 16 | 17 | let rawClient = new Client(DEFAULT_SERVER, CLIENT_OPTIONS); 18 | 19 | const handler = { 20 | get(target, prop) { 21 | if (prop === 'updateClient') { 22 | return address => { 23 | rawClient = new Client(address, CLIENT_OPTIONS); 24 | }; 25 | } 26 | return rawClient[prop]; 27 | }, 28 | }; 29 | 30 | const client = new Proxy({}, handler); 31 | 32 | export async function resolveTransaction(parsed, signer) { 33 | const props = await client.database.getDynamicGlobalProperties(); 34 | 35 | // resolve the decoded tx and params to a signable tx 36 | const { tx } = hiveuri.resolveTransaction(parsed.tx, parsed.params, { 37 | /* eslint-disable no-bitwise */ 38 | ref_block_num: props.head_block_number & 0xffff, 39 | ref_block_prefix: Buffer.from(props.head_block_id, 'hex').readUInt32LE(4), 40 | expiration: new Date(Date.now() + client.broadcast.expireTime + EXPIRE_TIME) 41 | .toISOString() 42 | .slice(0, -5), 43 | signers: [signer], 44 | preferred_signer: signer, 45 | }); 46 | tx.ref_block_num = parseInt(tx.ref_block_num, 10); 47 | tx.ref_block_prefix = parseInt(tx.ref_block_prefix, 10); 48 | 49 | return tx; 50 | } 51 | 52 | export default client; 53 | -------------------------------------------------------------------------------- /.github/workflows/master.yml: -------------------------------------------------------------------------------- 1 | name: Master CI/CD 2 | on: 3 | push: 4 | branches: 5 | - master 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | node-version: [8.x, 10.x, 12.x] 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Use Node.js ${{ matrix.node-version }} 15 | uses: actions/setup-node@v1 16 | with: 17 | node-version: ${{ matrix.node-version }} 18 | - name: npm install, build, lint and test 19 | run: | 20 | npm install 21 | npm run build --if-present 22 | npm run lint --if-present 23 | npm run test --if-present 24 | env: 25 | CI: true 26 | 27 | deploy: 28 | needs: build 29 | runs-on: ubuntu-latest 30 | steps: 31 | - name: SSH and deploy node app 32 | uses: appleboy/ssh-action@5711a203b3207eb1c6cebec6ac2152ab210ec3ae 33 | env: 34 | BROADCAST_URL: ${{secrets.BROADCAST_URL}} 35 | BROADCASTER_USERNAME: ${{secrets.BROADCASTER_USERNAME}} 36 | BROADCASTER_POSTING_WIF: ${{secrets.BROADCASTER_POSTING_WIF}} 37 | PRODUCTION_PORT: ${{secrets.PRODUCTION_PORT}} 38 | with: 39 | host: ${{ secrets.SSH_HOST }} 40 | username: ${{ secrets.SSH_USERNAME }} 41 | key: ${{ secrets.SSH_KEY }} 42 | port: ${{ secrets.SSH_PORT }} 43 | envs: BROADCAST_URL,BROADCASTER_USERNAME,BROADCASTER_POSTING_WIF,PRODUCTION_PORT 44 | script: | 45 | cd ~/hsproduction 46 | git pull origin master 47 | npm install 48 | npm run build 49 | export PORT=$PRODUCTION_PORT 50 | pm2 reload master 51 | -------------------------------------------------------------------------------- /.github/workflows/staging.yml: -------------------------------------------------------------------------------- 1 | name: Staging CI/CD 2 | on: 3 | push: 4 | branches: 5 | - development 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | node-version: [8.x, 10.x, 12.x] 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Use Node.js ${{ matrix.node-version }} 15 | uses: actions/setup-node@v1 16 | with: 17 | node-version: ${{ matrix.node-version }} 18 | - name: npm install, build, lint and test 19 | run: | 20 | npm install 21 | npm run build --if-present 22 | npm run lint --if-present 23 | npm run test --if-present 24 | env: 25 | CI: true 26 | 27 | deploy: 28 | needs: build 29 | runs-on: ubuntu-latest 30 | steps: 31 | - name: SSH and deploy node app 32 | uses: appleboy/ssh-action@5711a203b3207eb1c6cebec6ac2152ab210ec3ae 33 | env: 34 | BROADCAST_URL: ${{secrets.BROADCAST_URL}} 35 | BROADCASTER_USERNAME: ${{secrets.BROADCASTER_USERNAME}} 36 | BROADCASTER_POSTING_WIF: ${{secrets.BROADCASTER_POSTING_WIF}} 37 | STAGING_PORT: ${{secrets.STAGING_PORT}} 38 | with: 39 | host: ${{ secrets.SSH_HOST }} 40 | username: ${{ secrets.SSH_USERNAME }} 41 | key: ${{ secrets.SSH_KEY }} 42 | port: ${{ secrets.SSH_PORT }} 43 | envs: BROADCAST_URL,BROADCASTER_USERNAME,BROADCASTER_POSTING_WIF,STAGING_PORT 44 | script: | 45 | cd ~/hsstaging 46 | git pull origin development 47 | npm install 48 | npm run build 49 | export PORT=$STAGING_PORT 50 | pm2 reload staging 51 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import 'primer/index.scss'; 2 | import '@vue/ui/dist/vue-ui.css'; 3 | import '@/styles.less'; 4 | import Vue from 'vue'; 5 | import VueI18n from 'vue-i18n'; 6 | import VueUi from '@vue/ui'; 7 | import { upperFirst, camelCase } from 'lodash'; 8 | import urlParse from 'url-parse'; 9 | import App from '@/App.vue'; 10 | import router from '@/router'; 11 | import getPersistedData from '@/persisted'; 12 | import messages from '@/translation.json'; 13 | import numberFormats from '@/number.json'; 14 | import createIdleDetector from '@/helpers/idle'; 15 | 16 | // eslint-disable-next-line import/prefer-default-export 17 | export const idleDetector = createIdleDetector({ 18 | autostop: true, 19 | }); 20 | 21 | const requireComponent = require.context('./components', true, /[\w-]+\.vue$/); 22 | requireComponent.keys().forEach(fileName => { 23 | const componentConfig = requireComponent(fileName); 24 | const componentName = upperFirst(camelCase(fileName.replace(/^\.\//, '').replace(/\.\w+$/, ''))); 25 | Vue.component(componentName, componentConfig.default || componentConfig); 26 | }); 27 | 28 | Vue.filter('dateHeader', value => new Date(value).toLocaleString()); 29 | Vue.filter('parseUrl', value => urlParse(value).host); 30 | Vue.filter('pretty', value => { 31 | let json; 32 | try { 33 | json = JSON.stringify(JSON.parse(value), null, 2); 34 | } catch (e) { 35 | json = value; 36 | } 37 | return json; 38 | }); 39 | 40 | Vue.use(VueUi); 41 | Vue.use(VueI18n); 42 | 43 | getPersistedData(({ store }) => { 44 | store.dispatch('loadSettings'); 45 | 46 | const i18n = new VueI18n({ 47 | locale: 'en', 48 | messages, 49 | numberFormats, 50 | }); 51 | 52 | Vue.config.productionTip = false; 53 | 54 | new Vue({ 55 | i18n, 56 | router, 57 | store, 58 | render: h => h(App), 59 | }).$mount('#app'); 60 | }); 61 | -------------------------------------------------------------------------------- /src/views/About.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 59 | -------------------------------------------------------------------------------- /src/components/Operation/Value.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 55 | -------------------------------------------------------------------------------- /src/views/Accounts.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 73 | 74 | 79 | -------------------------------------------------------------------------------- /src/store/modules/settings.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import client from '@/helpers/client'; 3 | import { idleDetector } from '@/main'; 4 | 5 | const SETTINGS_KEY = 'settings'; 6 | 7 | const state = { 8 | properties: {}, 9 | steemAddressPrefix: '', 10 | chainId: '', 11 | language: 'en', 12 | timeout: '20', 13 | theme: 'white', 14 | address: 'https://api.hive.blog', 15 | }; 16 | 17 | const mutations = { 18 | saveProperties(_state, result) { 19 | Vue.set(_state, 'properties', result); 20 | }, 21 | saveConfig(_state, config) { 22 | Vue.set(_state, 'steemAddressPrefix', config.HIVE_ADDRESS_PREFIX); 23 | Vue.set(_state, 'chainId', config.HIVE_CHAIN_ID); 24 | }, 25 | saveSettings(_state, settings) { 26 | Vue.set(_state, 'language', settings.language || _state.language); 27 | Vue.set(_state, 'timeout', settings.timeout || _state.timeout); 28 | Vue.set(_state, 'theme', settings.theme || _state.theme); 29 | Vue.set(_state, 'address', settings.address || _state.address); 30 | }, 31 | }; 32 | 33 | const actions = { 34 | getDynamicGlobalProperties: ({ commit }) => 35 | client.database.call('get_dynamic_global_properties', []).then(result => { 36 | commit('saveProperties', result); 37 | }), 38 | getConfig: async ({ commit }) => { 39 | const config = await client.database.call('get_config', []); 40 | commit('saveConfig', config); 41 | }, 42 | loadSettings: ({ dispatch, commit }) => { 43 | const settingsContent = localStorage.getItem(SETTINGS_KEY); 44 | if (!settingsContent) { 45 | dispatch('getConfig'); 46 | return; 47 | } 48 | 49 | try { 50 | const settings = JSON.parse(settingsContent); 51 | client.updateClient(settings.address); 52 | dispatch('getConfig'); 53 | 54 | idleDetector.start(settings.timeout * 60 * 1000, () => { 55 | idleDetector.stop(); 56 | dispatch('logout'); 57 | }); 58 | 59 | commit('saveSettings', settings); 60 | } catch (err) { 61 | console.error("Couldn't load settings", err); 62 | } 63 | }, 64 | saveSettings: ({ dispatch }, settings) => { 65 | try { 66 | localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings)); 67 | } catch (err) { 68 | console.error("Couldn't save settings", err); 69 | } 70 | 71 | dispatch('loadSettings'); 72 | }, 73 | }; 74 | 75 | export default { 76 | state, 77 | mutations, 78 | actions, 79 | }; 80 | -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 15 | 16 | 18 | image/svg+xml 19 | 21 | steemconnect 22 | 23 | 24 | 25 | 27 | hivesigner 29 | 35 | 39 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/views/Auths.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 82 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ### Opening an issue 2 | 3 | You should usually open an issue in the following situations: 4 | 5 | * Report an error you can’t solve yourself 6 | * Discuss a high-level topic or idea (for example, community, vision or policies) 7 | * Propose a new feature or other project idea 8 | 9 | Tips for communicating on issues: 10 | 11 | * **If you see an open issue that you want to tackle,** comment on the issue to let people know you’re on it. That way, people are less likely to duplicate your work. 12 | * **If an issue was opened a while ago,** it’s possible that it’s being addressed somewhere else, or has already been resolved, so comment to ask for confirmation before starting work. 13 | * **If you opened an issue, but figured out the answer later on your own,** comment on the issue to let people know, then close the issue. Even documenting that outcome is a contribution to the project. 14 | 15 | ### Opening a pull request 16 | 17 | You should usually open a pull request in the following situations: 18 | 19 | * Submit trivial fixes (for example, a typo, a broken link or an obvious error) 20 | * Start work on a contribution that was already asked for, or that you’ve already discussed, in an issue 21 | 22 | A pull request doesn’t have to represent finished work. It’s usually better to open a pull request early on, so others can watch or give feedback on your progress. Just mark it as a “WIP” (Work in Progress) in the subject line. You can always add more commits later. 23 | 24 | If the project is on GitHub, here’s how to submit a pull request: 25 | 26 | * **Fork the repository** and clone it locally. Connect your local to the original repository by adding it as a remote. Pull in changes from this repository often so that you stay up to date so that when you submit your pull request, merge conflicts will be less likely. 27 | * **Create a branch** for your edits. 28 | * **Reference any relevant issues** or supporting documentation in your PR (for example, “Closes #37.”) 29 | * **Include screenshots of the before and after** if your changes include differences in HTML/CSS. Drag and drop the images into the body of your pull request. 30 | * **Test your changes!** Run your changes against any existing tests if they exist and create new ones when needed. Whether tests exist or not, make sure your changes don’t break the existing project. 31 | * **Contribute in the style of the project** to the best of your abilities. This may mean using indents, semi-colons or comments differently than you would in your own repository, but makes it easier for the maintainer to merge, others to understand and maintain in the future. 32 | -------------------------------------------------------------------------------- /src/fonts.less: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Circular-Pro-Book'; 3 | src: url('./assets/fonts/lineto-circular-pro-book.eot'); 4 | src: url('./assets/fonts/lineto-circular-pro-book.eot?#iefix') format('embedded-opentype'), 5 | url('./assets/fonts/lineto-circular-pro-book.woff') format('woff'), 6 | url('./assets/fonts/lineto-circular-pro-book.ttf') format('truetype'), 7 | url('./assets/fonts/lineto-circular-pro-book.svg#lineto-circular-pro-book') format('svg'); 8 | font-weight: normal; 9 | font-style: normal; 10 | } 11 | 12 | @font-face { 13 | font-family: 'Circular-Pro-Book-Italic'; 14 | src: url('./assets/fonts/lineto-circular-pro-bookItalic.eot'); 15 | src: url('./assets/fonts/lineto-circular-pro-bookItalic.eot?#iefix') format('embedded-opentype'), 16 | url('./assets/fonts/lineto-circular-pro-bookItalic.woff') format('woff'), 17 | url('./assets/fonts/lineto-circular-pro-bookItalic.ttf') format('truetype'), 18 | url('./assets/fonts/lineto-circular-pro-bookItalic.svg#lineto-circular-pro-book') format('svg'); 19 | font-weight: normal; 20 | font-style: normal; 21 | } 22 | 23 | @font-face { 24 | font-family: 'Circular-Pro-Medium'; 25 | src: url('./assets/fonts/lineto-circular-pro-medium.eot'); 26 | src: url('./assets/fonts/lineto-circular-pro-medium.eot?#iefix') format('embedded-opentype'), 27 | url('./assets/fonts/lineto-circular-pro-medium.woff') format('woff'), 28 | url('./assets/fonts/lineto-circular-pro-medium.ttf') format('truetype'), 29 | url('./assets/fonts/lineto-circular-pro-medium.svg#lineto-circular-pro-medium') format('svg'); 30 | font-weight: normal; 31 | font-style: normal; 32 | } 33 | 34 | @font-face { 35 | font-family: 'Circular-Pro-Bold'; 36 | src: url('./assets/fonts/lineto-circular-pro-bold.eot'); 37 | src: url('./assets/fonts/lineto-circular-pro-bold.eot?#iefix') format('embedded-opentype'), 38 | url('./assets/fonts/lineto-circular-pro-bold.woff') format('woff'), 39 | url('./assets/fonts/lineto-circular-pro-bold.ttf') format('truetype'), 40 | url('./assets/fonts/lineto-circular-pro-bold.svg#lineto-circular-pro-bold') format('svg'); 41 | font-weight: normal; 42 | font-style: normal; 43 | } 44 | 45 | @font-face { 46 | font-family: 'Circular-Pro-Black'; 47 | src: url('./assets/fonts/lineto-circular-black.eot'); 48 | src: url('./assets/fonts/lineto-circular-black.eot?#iefix') format('embedded-opentype'), 49 | url('./assets/fonts/lineto-circular-black.woff') format('woff'), 50 | url('./assets/fonts/lineto-circular-black.ttf') format('truetype'), 51 | url('./assets/fonts/lineto-circular-black.svg#lineto-circular-pro-black') format('svg'); 52 | font-weight: normal; 53 | font-style: normal; 54 | } 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hivesigner-ui", 3 | "version": "0.2.6", 4 | "description": "Secure way to sign with Hivesigner. Best security for users and developers to integrate industry standard OAuth2.", 5 | "homepage": "https://hivesigner.com", 6 | "main": "electron-entry.js", 7 | "author": "Fabien Marino (https://github.com/bonustrack)", 8 | "license": "MIT", 9 | "scripts": { 10 | "start": "node start.js", 11 | "serve": "vue-cli-service serve", 12 | "build": "vue-cli-service build", 13 | "lint": "vue-cli-service lint", 14 | "test": "vue-cli-service test:unit", 15 | "heroku-postbuild": "npm install --only=dev --no-shrinkwrap && npm run build" 16 | }, 17 | "dependencies": { 18 | "@hiveio/dhive": "^0.14.12", 19 | "@vue/ui": "^0.9.2", 20 | "bs58": "^4.0.1", 21 | "core-js": "^2.5.7", 22 | "express": "^4.16.4", 23 | "frameguard": "^3.1.0", 24 | "helmet-csp": "^2.7.1", 25 | "hive-uri": "^0.2.2", 26 | "lodash": "^4.17.19", 27 | "password-validator": "^4.1.1", 28 | "primer": "^11.0.0", 29 | "query-string": "^6.5.0", 30 | "serve-static": "^1.14.0", 31 | "triplesec": "^4.0.3", 32 | "url-parse": "^1.4.7", 33 | "verror": "1.10.0", 34 | "vue": "^2.6.10", 35 | "vue-i18n": "^8.11.2", 36 | "vue-router": "^3.0.6", 37 | "vuex": "^3.1.0" 38 | }, 39 | "devDependencies": { 40 | "@types/jest": "^24.0.11", 41 | "@vue/cli-plugin-babel": "^3.5.1", 42 | "@vue/cli-plugin-eslint": "^3.5.1", 43 | "@vue/cli-plugin-unit-jest": "^3.7.0", 44 | "@vue/cli-service": "^3.7.0", 45 | "@vue/eslint-config-airbnb": "^4.0.0", 46 | "@vue/test-utils": "^1.0.0-beta.29", 47 | "babel-core": "^6.26.3", 48 | "babel-jest": "^24.5.0", 49 | "eslint-config-prettier": "^4.1.0", 50 | "eslint-plugin-prettier": "^3.0.1", 51 | "less": "^3.9.0", 52 | "less-loader": "^5.0.0", 53 | "node-sass": "^4.12.0", 54 | "prettier": "^1.17.0", 55 | "sass-loader": "^7.1.0", 56 | "vue-template-compiler": "^2.6.10" 57 | }, 58 | "contributors": [ 59 | "Fabien Marino (https://github.com/bonustrack)", 60 | "Wiktor Tkaczyński (https://github.com/Sekhmet)", 61 | "Johan Nordberg (https://github.com/jnordberg)", 62 | "Nico Wehmöller (https://github.com/wehmoen)", 63 | "Mahdi Yari (https://github.com/mahdiyari)", 64 | "Feruz Muradov (https://github.com/feruzm)" 65 | ], 66 | "repository": { 67 | "type": "git", 68 | "url": "https://github.com/ledgerconnect/hivesigner.git" 69 | }, 70 | "bugs": { 71 | "url": "https://github.com/ledgerconnect/hivesigner/issues" 72 | }, 73 | "browserslist": [ 74 | "> 1%", 75 | "last 2 versions", 76 | "not ie <= 8" 77 | ] 78 | } 79 | -------------------------------------------------------------------------------- /src/components/Modal/Profile.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 85 | -------------------------------------------------------------------------------- /src/helpers/auth.js: -------------------------------------------------------------------------------- 1 | import { PrivateKey } from '@hiveio/dhive'; 2 | import * as bs58 from 'bs58'; 3 | import client from './client'; 4 | 5 | function decodePrivate(encodedKey) { 6 | const buffer = bs58.decode(encodedKey); 7 | 8 | if (buffer[0] !== 128) throw new Error('private key network id mismatch'); 9 | 10 | return buffer.slice(0, -4); 11 | } 12 | 13 | export function privateKeyFrom(password) { 14 | return new PrivateKey(decodePrivate(password).slice(1)); 15 | } 16 | 17 | function isKey(username, password) { 18 | try { 19 | privateKeyFrom(password); 20 | return true; 21 | } catch (err) { 22 | return false; 23 | } 24 | } 25 | 26 | async function getUserKeysMap(username) { 27 | const keys = {}; 28 | 29 | let accounts = null; 30 | try { 31 | accounts = await client.database.getAccounts([username]); 32 | } catch (err) { 33 | console.error('Error getting data from chain', err); 34 | return keys; 35 | } 36 | 37 | if (accounts.length !== 1) return keys; 38 | 39 | const [account] = accounts; 40 | 41 | keys[account.memo_key] = 'memo'; 42 | 43 | const types = ['owner', 'active', 'posting']; 44 | 45 | for (let i = 0; i < types.length; i += 1) { 46 | const keysOfType = account[types[i]].key_auths; 47 | 48 | for (let j = 0; j < keysOfType.length; j += 1) { 49 | keys[keysOfType[j][0]] = types[i]; 50 | } 51 | } 52 | 53 | return keys; 54 | } 55 | 56 | export async function credentialsValid(username, password) { 57 | const keysMap = await getUserKeysMap(username); 58 | 59 | const key = isKey(username, password) 60 | ? privateKeyFrom(password) 61 | : PrivateKey.fromLogin(username, password, 'active'); 62 | 63 | return !!keysMap[key.createPublic().toString()]; 64 | } 65 | 66 | export async function getKeys(username, password) { 67 | const keys = { 68 | active: null, 69 | memo: null, 70 | posting: null, 71 | }; 72 | 73 | const keysMap = await getUserKeysMap(username); 74 | 75 | if (isKey(username, password)) { 76 | const type = 77 | keysMap[ 78 | privateKeyFrom(password) 79 | .createPublic() 80 | .toString() 81 | ]; 82 | 83 | keys[type] = password; 84 | 85 | return keys; 86 | } 87 | const ownerKey = PrivateKey.fromLogin(username, password, 'owner'); 88 | const activeKey = PrivateKey.fromLogin(username, password, 'active'); 89 | const postingKey = PrivateKey.fromLogin(username, password, 'posting'); 90 | const memoKey = PrivateKey.fromLogin(username, password, 'memo'); 91 | 92 | keys.owner = ownerKey.toString(); 93 | keys.active = activeKey.toString(); 94 | keys.posting = postingKey.toString(); 95 | 96 | if (keysMap[memoKey.createPublic().toString()] === 'memo') { 97 | keys.memo = memoKey.toString(); 98 | } 99 | 100 | return keys; 101 | } 102 | 103 | export function getAuthority(str, fallback) { 104 | return ['owner', 'active', 'posting'].includes(str) ? str : fallback; 105 | } 106 | -------------------------------------------------------------------------------- /src/store/modules/auth.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import { cryptoUtils } from '@hiveio/dhive'; 3 | import client from '@/helpers/client'; 4 | import { credentialsValid, privateKeyFrom } from '@/helpers/auth'; 5 | import router from '@/router'; 6 | import { idleDetector } from '@/main'; 7 | 8 | const state = { 9 | username: null, 10 | keys: {}, 11 | account: {}, 12 | }; 13 | 14 | const mutations = { 15 | login(_state, { result, keys }) { 16 | Vue.set(_state, 'username', result.name); 17 | Vue.set(_state, 'keys', keys); 18 | Vue.set(_state, 'account', result); 19 | }, 20 | logout(_state) { 21 | Vue.set(_state, 'username', null); 22 | Vue.set(_state, 'keys', {}); 23 | Vue.set(_state, 'account', {}); 24 | }, 25 | loadAccount(_state, account) { 26 | Vue.set(_state, 'account', account); 27 | }, 28 | }; 29 | 30 | const actions = { 31 | login: async ({ commit, dispatch, rootState }, { username, keys }) => { 32 | const key = keys.owner || keys.active || keys.posting || keys.memo; 33 | const valid = await credentialsValid(username, key); 34 | 35 | if (!valid) { 36 | throw new Error('Invalid credentials'); 37 | } 38 | 39 | const result = await client.database.getAccounts([username]); 40 | commit('login', { result: result[0], keys }); 41 | 42 | idleDetector.start(rootState.settings.timeout * 60 * 1000, () => { 43 | idleDetector.stop(); 44 | dispatch('logout'); 45 | }); 46 | }, 47 | logout: ({ commit }) => { 48 | commit('logout'); 49 | router.push('/'); 50 | }, 51 | loadAccount: async ({ commit, rootState }) => { 52 | const { username } = rootState.auth; 53 | const [account] = await client.database.getAccounts([username]); 54 | commit('loadAccount', account); 55 | }, 56 | sign: ({ rootState }, { tx, authority }) => { 57 | const { keys } = rootState.auth; 58 | const { chainId } = rootState.settings; 59 | const privateKey = 60 | authority && keys[authority] 61 | ? privateKeyFrom(keys[authority]) 62 | : privateKeyFrom(keys.owner || keys.active || keys.posting || keys.memo); 63 | return cryptoUtils.signTransaction(tx, [privateKey], Buffer.from(chainId, 'hex')); 64 | }, 65 | signMessage: ({ rootState }, { message, authority }) => { 66 | const { keys, username } = rootState.auth; 67 | const timestamp = parseInt(new Date().getTime() / 1000, 10); 68 | const messageObj = { signed_message: message, authors: [username], timestamp }; 69 | const hash = cryptoUtils.sha256(JSON.stringify(messageObj)); 70 | const privateKey = 71 | authority && keys[authority] 72 | ? privateKeyFrom(keys[authority]) 73 | : privateKeyFrom(keys.owner || keys.active || keys.posting || keys.memo); 74 | const signature = privateKey.sign(hash).toString(); 75 | messageObj.signatures = [signature]; 76 | return messageObj; 77 | }, 78 | broadcast: (context, tx) => client.broadcast.send(tx), 79 | updateAccount: ({ rootState }, data) => { 80 | const { keys } = rootState.auth; 81 | const privateKey = privateKeyFrom(keys.owner || keys.active); 82 | return client.broadcast.updateAccount(data, privateKey); 83 | }, 84 | }; 85 | 86 | export default { 87 | state, 88 | mutations, 89 | actions, 90 | }; 91 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at fabien@bonustrack.co. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /src/views/Settings.vue: -------------------------------------------------------------------------------- 1 | 69 | 70 | 119 | -------------------------------------------------------------------------------- /src/views/Apps.vue: -------------------------------------------------------------------------------- 1 | 64 | 65 | 127 | 128 | 147 | -------------------------------------------------------------------------------- /src/views/Revoke.vue: -------------------------------------------------------------------------------- 1 | 74 | 75 | 156 | -------------------------------------------------------------------------------- /src/helpers/utils.js: -------------------------------------------------------------------------------- 1 | import { has, snakeCase } from 'lodash'; 2 | import urlParse from 'url-parse'; 3 | import qs from 'query-string'; 4 | import { encodeOps, decode } from 'hive-uri'; 5 | import operations from '@/helpers/operations.json'; 6 | 7 | export const REQUEST_ID_PARAM = 'requestId'; 8 | 9 | export const isChromeExtension = () => false; 10 | 11 | export const isWeb = () => !isChromeExtension(); 12 | 13 | export function jsonParse(input, fallback) { 14 | try { 15 | return JSON.parse(input); 16 | } catch (err) { 17 | return fallback || {}; 18 | } 19 | } 20 | 21 | /** Parse error message from hived response */ 22 | export function getErrorMessage(error) { 23 | let errorMessage = ''; 24 | if (has(error, 'stack[0].format')) { 25 | errorMessage = error.stack[0].format; 26 | if (has(error, 'stack[0].data')) { 27 | const { data } = error.stack[0]; 28 | Object.keys(data).forEach(d => { 29 | errorMessage = errorMessage.split(`\${${d}}`).join(data[d]); 30 | }); 31 | } 32 | } else if (error.message) { 33 | errorMessage = error.message; 34 | } 35 | return errorMessage; 36 | } 37 | 38 | export function getVestsToSP(properties) { 39 | return ( 40 | parseFloat(properties.total_vesting_fund_hive) / parseFloat(properties.total_vesting_shares) 41 | ); 42 | } 43 | 44 | export function legacyToHiveUri(uri) { 45 | let parsed; 46 | try { 47 | const url = urlParse(uri); 48 | const opName = snakeCase(url.pathname.slice(1)); 49 | const queryParams = qs.parse(url.query.slice(1)); 50 | if (operations[opName]) { 51 | const opParams = Object.keys(operations[opName].schema).reduce((acc, b) => { 52 | if (!queryParams[b]) return acc; 53 | let value = queryParams[b]; 54 | if (operations[opName].schema[b] && operations[opName].schema[b].type) { 55 | if (['array', 'object'].includes(operations[opName].schema[b].type)) 56 | value = jsonParse(value, value); 57 | if (operations[opName].schema[b].type === 'bool') 58 | value = ['true', true, 1, '1'].includes(value); 59 | } 60 | return { ...acc, [b]: value }; 61 | }, {}); 62 | const params = { callback: queryParams.redirect_uri }; 63 | const b64Uri = encodeOps([[opName, opParams]], params); 64 | parsed = decode(b64Uri); 65 | } 66 | } catch (err) { 67 | console.log('Failed to parse legacy uri', err); 68 | } 69 | return parsed; 70 | } 71 | 72 | function processValue(schema, key, value, { vestsToSP }) { 73 | const { type, defaultValue, maxLength } = schema[key]; 74 | const realValue = !value && typeof defaultValue !== 'undefined' ? defaultValue : value; 75 | switch (type) { 76 | case 'amount': 77 | if (realValue.indexOf('VESTS') !== -1) return `${parseFloat(realValue).toFixed(6)} VESTS`; 78 | if (realValue.indexOf('HP') !== -1) 79 | return `${(parseFloat(realValue) / vestsToSP).toFixed(6)} VESTS`; 80 | if (realValue.indexOf('HIVE') !== -1) return `${parseFloat(realValue).toFixed(3)} HIVE`; 81 | if (realValue.indexOf('HBD') !== -1) return `${parseFloat(realValue).toFixed(3)} HBD`; 82 | return realValue; 83 | case 'int': 84 | return parseInt(realValue, 10); 85 | case 'bool': 86 | if (value === 'false' || value === false) return false; 87 | return realValue; 88 | case 'string': 89 | if (maxLength) return realValue.substring(0, Math.min(realValue.length, maxLength - 1)); 90 | return realValue; 91 | default: 92 | return realValue; 93 | } 94 | } 95 | 96 | export function processTransaction(transaction, config) { 97 | const processed = { ...transaction }; 98 | processed.tx.operations = transaction.tx.operations.map(([name, payload]) => { 99 | const processedPayload = Object.keys(operations[name].schema).reduce( 100 | (acc, key) => ({ 101 | ...acc, 102 | [key]: processValue(operations[name].schema, key, payload[key], config), 103 | }), 104 | {}, 105 | ); 106 | return [name, processedPayload]; 107 | }); 108 | return processed; 109 | } 110 | 111 | export function formatNumber(number) { 112 | if (parseFloat(number.toFixed(6)) < 0.001) { 113 | return number.toFixed(6); 114 | } 115 | return number.toFixed(3); 116 | } 117 | 118 | export function buildSearchParams(route) { 119 | const keys = Object.keys(route.query); 120 | if (keys.length === 0) return ''; 121 | const params = keys 122 | .filter(key => key !== REQUEST_ID_PARAM) 123 | .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(route.query[key])}`) 124 | .join('&'); 125 | return `?${params}`; 126 | } 127 | 128 | export function signComplete() { 129 | if (!isChromeExtension()) return; 130 | window.close(); 131 | } 132 | 133 | export function isValidUrl(string) { 134 | try { 135 | // eslint-disable-next-line no-new 136 | new URL(string); 137 | return true; 138 | } catch (e) { 139 | return false; 140 | } 141 | } 142 | 143 | export function getLowestAuthorityRequired(tx) { 144 | let authority; 145 | tx.operations.forEach(operation => { 146 | if (operations[operation[0]] && operations[operation[0]].authority) { 147 | if (operations[operation[0]].authority === 'owner') authority = 'owner'; 148 | if (operations[operation[0]].authority === 'active') authority = 'active'; 149 | if (operations[operation[0]].authority === 'posting' && authority !== 'active') { 150 | authority = 'posting'; 151 | } 152 | } 153 | }); 154 | return authority; 155 | } 156 | 157 | const b64uLookup = { '/': '_', _: '/', '+': '-', '-': '+', '=': '.', '.': '=' }; 158 | 159 | export const b64uEnc = str => btoa(str).replace(/(\+|\/|=)/g, m => b64uLookup[m]); 160 | 161 | export const b64uDec = str => atob(str.replace(/(-|_|\.)/g, m => b64uLookup[m])); 162 | -------------------------------------------------------------------------------- /src/router.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Router from 'vue-router'; 3 | import store from '@/store'; 4 | import { isWeb } from '@/helpers/utils'; 5 | import { hasAccounts } from '@/helpers/keychain'; 6 | 7 | const Home = () => import(/* webpackChunkName: "home" */ '@/views/Home.vue'); 8 | const Import = () => import(/* webpackChunkName: "import" */ '@/views/Import.vue'); 9 | const Login = () => import(/* webpackChunkName: "login" */ '@/views/Login.vue'); 10 | const Dashboard = () => import(/* webpackChunkName: "dashboard" */ '@/views/Dashboard.vue'); 11 | const Auths = () => import(/* webpackChunkName: "auths" */ '@/views/Auths.vue'); 12 | // const LoginRequest = () => import(/* webpackChunkName: "login-request" */ '@/views/LoginRequest.vue'); 13 | const Sign = () => import(/* webpackChunkName: "sign" */ '@/views/Sign.vue'); 14 | const Authorize = () => import(/* webpackChunkName: "authorize" */ '@/views/Authorize.vue'); 15 | const Revoke = () => import(/* webpackChunkName: "revoke" */ '@/views/Revoke.vue'); 16 | const Profile = () => import(/* webpackChunkName: "profile" */ '@/views/Profile.vue'); 17 | const Settings = () => import(/* webpackChunkName: "settings" */ '@/views/Settings.vue'); 18 | const Accounts = () => import(/* webpackChunkName: "accounts" */ '@/views/Accounts.vue'); 19 | const About = () => import(/* webpackChunkName: "about" */ '@/views/About.vue'); 20 | const Apps = () => import(/* webpackChunkName: "apps" */ '@/views/Apps.vue'); 21 | const Developers = () => import(/* webpackChunkName: "developers" */ '@/views/Developers.vue'); 22 | const Error404 = () => import(/* webpachChunkName: "error-404" */ '@/views/404.vue'); 23 | 24 | Vue.use(Router); 25 | 26 | const requireAuth = (to, from, next, params) => { 27 | if (!store.state.auth.account.name) { 28 | const name = hasAccounts() ? 'login' : 'import'; 29 | const redirect = to.fullPath === '/' ? undefined : to.fullPath; 30 | const query = { redirect }; 31 | if (params && params.authority) query.authority = params.authority; 32 | next({ name, query }); 33 | } else { 34 | next(); 35 | } 36 | }; 37 | 38 | const beforeLogin = (to, from, next) => { 39 | if (!hasAccounts()) { 40 | const redirect = to.query.redirect === '/' ? undefined : to.query.redirect; 41 | const authority = to.query.authority || undefined; 42 | next({ name: 'import', query: { redirect, authority } }); 43 | } else { 44 | next(); 45 | } 46 | }; 47 | 48 | const redirectToLoginRequest = (to, from, next) => { 49 | const { query } = to; 50 | const clientId = query.client_id; 51 | delete query.client_id; 52 | let scope = 'posting'; 53 | if (query.scope === 'login') scope = 'login'; 54 | if (query.scope && query.scope.includes('offline')) { 55 | scope = 'posting'; 56 | query.response_type = 'code'; 57 | } 58 | query.scope = scope; 59 | next({ name: 'login-request-app', params: { clientId }, query }); 60 | }; 61 | 62 | export default new Router({ 63 | mode: isWeb() ? 'history' : 'hash', 64 | scrollBehavior() { 65 | return { x: 0, y: 0 }; 66 | }, 67 | routes: [ 68 | { 69 | path: '/', 70 | name: isWeb() ? 'home' : 'dashboard', 71 | beforeEnter: isWeb() ? null : requireAuth, 72 | component: isWeb() ? Home : Dashboard, 73 | }, 74 | { 75 | path: '/import', 76 | name: 'import', 77 | component: Import, 78 | }, 79 | { 80 | path: '/login', 81 | name: 'login', 82 | beforeEnter: beforeLogin, 83 | component: Login, 84 | }, 85 | { 86 | path: '/auths', 87 | name: 'auths', 88 | beforeEnter: requireAuth, 89 | component: Auths, 90 | }, 91 | { 92 | path: '/oauth2/authorize', 93 | beforeEnter: redirectToLoginRequest, 94 | }, 95 | { 96 | path: '/login-request', 97 | name: 'login-request', 98 | redirect: to => ({ 99 | name: 'login', 100 | query: { 101 | redirect: to.fullPath, 102 | }, 103 | }), 104 | }, 105 | { 106 | path: '/login-request/:clientId', 107 | name: 'login-request-app', 108 | redirect: to => ({ 109 | name: 'login', 110 | query: { 111 | redirect: to.fullPath, 112 | }, 113 | }), 114 | }, 115 | { 116 | path: '/sign/*', 117 | name: 'sign', 118 | component: Sign, 119 | }, 120 | { 121 | path: '/authorize/@:username', 122 | redirect: to => ({ 123 | name: 'authorize', 124 | params: { 125 | username: to.params.username, 126 | }, 127 | }), 128 | }, 129 | { 130 | path: '/authorize/:username', 131 | name: 'authorize', 132 | component: Authorize, 133 | }, 134 | { 135 | path: '/revoke/@:username', 136 | redirect: to => ({ 137 | name: 'revoke', 138 | params: { 139 | username: to.params.username, 140 | }, 141 | }), 142 | }, 143 | { 144 | path: '/revoke/:username', 145 | name: 'revoke', 146 | component: Revoke, 147 | }, 148 | { 149 | path: '/profile', 150 | name: 'profile', 151 | beforeEnter: requireAuth, 152 | component: Profile, 153 | }, 154 | { 155 | path: '/settings', 156 | name: 'settings', 157 | component: Settings, 158 | }, 159 | { 160 | path: '/accounts', 161 | name: 'accounts', 162 | component: Accounts, 163 | }, 164 | { 165 | path: '/about', 166 | name: 'about', 167 | component: About, 168 | }, 169 | { 170 | path: '/apps', 171 | name: 'apps', 172 | component: Apps, 173 | }, 174 | { 175 | path: '/developers', 176 | name: 'developers', 177 | component: Developers, 178 | }, 179 | { 180 | path: '*', 181 | component: Error404, 182 | name: 'error-404', 183 | }, 184 | ], 185 | }); 186 | -------------------------------------------------------------------------------- /src/styles.less: -------------------------------------------------------------------------------- 1 | @import './vars'; 2 | @import './fonts'; 3 | 4 | * { 5 | outline: none; 6 | } 7 | 8 | html { 9 | height: 100%; 10 | } 11 | 12 | body { 13 | display: flex; 14 | color: @text-color; 15 | background-color: @bg-color; 16 | font-size: 16px; 17 | } 18 | 19 | .app--extension { 20 | height: 600px; 21 | width: @extension-width !important; 22 | } 23 | 24 | body, 25 | #app { 26 | min-height: 100%; 27 | width: 100%; 28 | overflow-x: hidden; 29 | } 30 | 31 | a { 32 | color: @link-color; 33 | cursor: pointer; 34 | } 35 | 36 | p { 37 | font-size: 18px; 38 | } 39 | 40 | h1, 41 | h2, 42 | h3, 43 | h4, 44 | h5 { 45 | color: @heading-color; 46 | } 47 | 48 | h1, 49 | h2, 50 | h3, 51 | h4, 52 | b, 53 | .btn { 54 | font-family: 'Circular-Pro-Bold', Helvetica, Arial, sans-serif; 55 | } 56 | 57 | label { 58 | color: @heading-color; 59 | display: inline-block; 60 | font-size: 18px; 61 | font-weight: 500; 62 | margin: 0 0 8px; 63 | } 64 | 65 | pre { 66 | font-size: 15px; 67 | } 68 | 69 | #app { 70 | font-family: 'Circular-Pro-Book', Helvetica, Arial, sans-serif; 71 | color: @text-color; 72 | text-align: left !important; 73 | } 74 | 75 | @media (max-width: 400px) { 76 | & .Box { 77 | border-radius: 0; 78 | border: none; 79 | 80 | &.operation { 81 | margin: -24px; 82 | 83 | & .Box-row { 84 | padding: 16px 24px; 85 | } 86 | } 87 | } 88 | } 89 | 90 | .after-header { 91 | margin-top: @header-height; 92 | 93 | @media @bp-small { 94 | margin-top: 0; 95 | } 96 | } 97 | 98 | .table { 99 | border-radius: 4px; 100 | margin-bottom: 24px; 101 | font-size: 17px; 102 | 103 | th, 104 | td { 105 | padding: 12px 24px; 106 | } 107 | 108 | th { 109 | font-weight: normal; 110 | } 111 | 112 | thead { 113 | color: @border-color; 114 | } 115 | } 116 | 117 | .entry-enter-active { 118 | transition: opacity 1s ease; 119 | } 120 | 121 | .entry-enter { 122 | opacity: 0; 123 | } 124 | 125 | .entry-leave { 126 | display: none; 127 | } 128 | 129 | ::-webkit-scrollbar-thumb { 130 | background-color: @border-color; 131 | border: 0; 132 | background-clip: padding-box; 133 | border-radius: 0; 134 | } 135 | 136 | .vue-ui-modal { 137 | .backdrop { 138 | background-color: rgba(0, 0, 0, 0.8); 139 | } 140 | 141 | .shell { 142 | transition: none; 143 | position: absolute; 144 | top: 0; 145 | bottom: 0; 146 | right: 0; 147 | border-radius: 0; 148 | max-height: inherit; 149 | width: 100% !important; 150 | max-width: 500px !important; 151 | min-width: inherit !important; 152 | border-left: 1px solid @border-color; 153 | 154 | .header { 155 | border-bottom: 1px solid @border-color; 156 | } 157 | 158 | .body { 159 | font-size: 18px; 160 | } 161 | } 162 | } 163 | 164 | .container-xs { 165 | width: 100%; 166 | max-width: 380px; 167 | margin-right: auto; 168 | margin-left: auto; 169 | } 170 | 171 | .container-sm { 172 | width: 100%; 173 | max-width: 544px; 174 | margin-right: auto; 175 | margin-left: auto; 176 | } 177 | 178 | .form-select { 179 | font-size: 16px; 180 | } 181 | 182 | .error { 183 | color: @error-color; 184 | } 185 | 186 | .button-link { 187 | line-height: 32px; 188 | text-decoration: none; 189 | 190 | &:link, 191 | &:visited, 192 | &:hover, 193 | &:active { 194 | text-decoration: none; 195 | } 196 | } 197 | 198 | input { 199 | background: transparent; 200 | } 201 | 202 | .form-label { 203 | min-width: 160px; 204 | display: inline-block; 205 | } 206 | 207 | .link-color { 208 | color: @link-color; 209 | } 210 | 211 | .primary-color { 212 | color: @primary-color; 213 | } 214 | 215 | .bg-primary { 216 | background-color: @primary-color; 217 | } 218 | 219 | .header-container { 220 | display: flex; 221 | flex: 1; 222 | 223 | & > div { 224 | flex: 1; 225 | } 226 | 227 | & > button { 228 | color: @border-color; 229 | font-size: 18px; 230 | padding: 0 24px; 231 | background: none; 232 | outline: none; 233 | border: none; 234 | } 235 | } 236 | 237 | .logo { 238 | font-size: 32px; 239 | color: @primary-color; 240 | } 241 | 242 | a.iconfont:hover { 243 | text-decoration: none; 244 | } 245 | 246 | .no-decoration, .no-decoration:hover { 247 | text-decoration: none; 248 | } 249 | 250 | .btn { 251 | background: transparent; 252 | color: darken(@input-border-color, 10%); 253 | border-color: @input-border-color; 254 | box-shadow: none !important; 255 | 256 | &:hover { 257 | color: @heading-color; 258 | background: transparent; 259 | } 260 | 261 | &:disabled { 262 | opacity: 0.4; 263 | } 264 | } 265 | 266 | .btn-blue, 267 | .btn-blue:focus, 268 | .btn-blue:disabled, 269 | .btn-blue:hover { 270 | color: #ffffff; 271 | border-color: @primary-color; 272 | background: @primary-color; 273 | 274 | &:hover { 275 | border-color: darken(@primary-color, 10%); 276 | background: darken(@primary-color, 10%); 277 | } 278 | } 279 | 280 | .btn-danger, 281 | .btn-danger:focus, 282 | .btn-danger:disabled, 283 | .btn-danger:hover { 284 | color: #ffffff; 285 | border-color: @error-color; 286 | background: @error-color; 287 | 288 | &:hover { 289 | border-color: darken(@error-color, 10%); 290 | background: darken(@error-color, 10%); 291 | } 292 | } 293 | 294 | .btn-success, 295 | .btn-success:focus, 296 | .btn-success:disabled, 297 | .btn-success:hover { 298 | color: #ffffff; 299 | border-color: @success-color; 300 | background: @success-color; 301 | 302 | &:hover { 303 | border-color: darken(@success-color, 10%); 304 | background: darken(@success-color, 10%); 305 | } 306 | } 307 | 308 | .input-lg { 309 | border-color: @input-border-color; 310 | padding: 6px 12px; 311 | min-height: 42px; 312 | box-shadow: none !important; 313 | 314 | &:focus { 315 | border-color: @primary-color; 316 | } 317 | } 318 | 319 | .hero { 320 | background-image: url('./assets/img/bg.svg'); 321 | } 322 | -------------------------------------------------------------------------------- /src/views/Authorize.vue: -------------------------------------------------------------------------------- 1 | 78 | 79 | 163 | -------------------------------------------------------------------------------- /src/assets/img/shapes.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Artboard 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/views/Sign.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 200 | -------------------------------------------------------------------------------- /src/views/LoginRequest.vue: -------------------------------------------------------------------------------- 1 | 57 | 58 | 203 | -------------------------------------------------------------------------------- /src/views/Developers.vue: -------------------------------------------------------------------------------- 1 | 158 | 159 | 176 | -------------------------------------------------------------------------------- /src/views/Profile.vue: -------------------------------------------------------------------------------- 1 | 136 | 137 | 219 | -------------------------------------------------------------------------------- /src/helpers/operations.json: -------------------------------------------------------------------------------- 1 | { 2 | "transfer": { 3 | "name": "Transfer", 4 | "authority": "active", 5 | "description": "Transfers asset from one account to another.", 6 | "schema": { 7 | "from": { 8 | "type": "account", 9 | "defaultValue": "__signer" 10 | }, 11 | "to": { 12 | "type": "account" 13 | }, 14 | "amount": { 15 | "type": "amount" 16 | }, 17 | "memo": { 18 | "type": "string", 19 | "defaultValue": "", 20 | "maxLength": 2048 21 | } 22 | } 23 | }, 24 | "delegate_vesting_shares": { 25 | "name": "Delegate Hive Power", 26 | "authority": "active", 27 | "schema": { 28 | "delegator": { 29 | "type": "account", 30 | "defaultValue": "__signer" 31 | }, 32 | "delegatee": { 33 | "type": "account" 34 | }, 35 | "vesting_shares": { 36 | "type": "amount" 37 | } 38 | } 39 | }, 40 | "transfer_to_vesting": { 41 | "name": "Power up", 42 | "authority": "active", 43 | "schema": { 44 | "from": { 45 | "type": "account", 46 | "defaultValue": "__signer" 47 | }, 48 | "to": { 49 | "type": "account", 50 | "defaultValue": "__signer" 51 | }, 52 | "amount": { 53 | "type": "amount" 54 | } 55 | } 56 | }, 57 | "set_withdraw_vesting_route": { 58 | "name": "Set withdraw vesting route", 59 | "authority": "active", 60 | "schema": { 61 | "from_account": { 62 | "type": "account", 63 | "defaultValue": "__signer" 64 | }, 65 | "to_account": { 66 | "type": "account" 67 | }, 68 | "percent": { 69 | "type": "int" 70 | }, 71 | "auto_vest": { 72 | "type": "bool", 73 | "defaultValue": false 74 | } 75 | } 76 | }, 77 | "withdraw_vesting": { 78 | "name": "Power down", 79 | "authority": "active", 80 | "schema": { 81 | "account": { 82 | "type": "account", 83 | "defaultValue": "__signer" 84 | }, 85 | "vesting_shares": { 86 | "type": "amount" 87 | } 88 | } 89 | }, 90 | "transfer_to_savings": { 91 | "name": "Transfer to saving", 92 | "authority": "active", 93 | "schema": { 94 | "from": { 95 | "type": "account", 96 | "defaultValue": "__signer" 97 | }, 98 | "to": { 99 | "type": "account", 100 | "defaultValue": "__signer" 101 | }, 102 | "amount": { 103 | "type": "amount" 104 | }, 105 | "memo": { 106 | "type": "string", 107 | "defaultValue": "", 108 | "maxLength": 2048 109 | } 110 | } 111 | }, 112 | "transfer_from_savings": { 113 | "name": "Transfer from saving", 114 | "authority": "active", 115 | "schema": { 116 | "from": { 117 | "type": "account", 118 | "defaultValue": "__signer" 119 | }, 120 | "to": { 121 | "type": "account", 122 | "defaultValue": "__signer" 123 | }, 124 | "amount": { 125 | "type": "amount" 126 | }, 127 | "memo": { 128 | "type": "string", 129 | "defaultValue": "", 130 | "maxLength": 2048 131 | }, 132 | "request_id": { 133 | "type": "int" 134 | } 135 | } 136 | }, 137 | "cancel_transfer_from_savings": { 138 | "name": "Cancel transfer from saving", 139 | "authority": "active", 140 | "schema": { 141 | "from": { 142 | "type": "account", 143 | "defaultValue": "__signer" 144 | }, 145 | "request_id": { 146 | "type": "int" 147 | } 148 | } 149 | }, 150 | "convert": { 151 | "name": "Convert", 152 | "authority": "active", 153 | "schema": { 154 | "owner": { 155 | "type": "account", 156 | "defaultValue": "__signer" 157 | }, 158 | "requestid": { 159 | "type": "int" 160 | }, 161 | "amount": { 162 | "type": "amount" 163 | } 164 | } 165 | }, 166 | "account_witness_vote": { 167 | "name": "Witness vote", 168 | "authority": "active", 169 | "schema": { 170 | "account": { 171 | "type": "account", 172 | "defaultValue": "__signer" 173 | }, 174 | "witness": { 175 | "type": "account" 176 | }, 177 | "approve": { 178 | "type": "bool", 179 | "defaultValue": true 180 | } 181 | } 182 | }, 183 | "account_witness_proxy": { 184 | "name": "Witness proxy", 185 | "authority": "active", 186 | "schema": { 187 | "account": { 188 | "type": "account", 189 | "defaultValue": "__signer" 190 | }, 191 | "proxy": { 192 | "type": "account" 193 | } 194 | } 195 | }, 196 | "claim_account": { 197 | "name": "Claim account", 198 | "authority": "active", 199 | "schema": { 200 | "creator": { 201 | "type": "account", 202 | "defaultValue": "__signer" 203 | }, 204 | "fee": { 205 | "type": "amount", 206 | "defaultValue": "0.000 HIVE" 207 | }, 208 | "extensions": { 209 | "type": "array", 210 | "defaultValue": [] 211 | } 212 | } 213 | }, 214 | "account_create": { 215 | "name": "Create account", 216 | "authority": "active", 217 | "schema": { 218 | "creator": { 219 | "type": "account", 220 | "defaultValue": "__signer" 221 | }, 222 | "fee": { 223 | "type": "amount", 224 | "defaultValue": "3.000 HIVE" 225 | }, 226 | "new_account_name": { 227 | "type": "account" 228 | }, 229 | "memo_key": { 230 | "type": "string" 231 | }, 232 | "json_metadata": { 233 | "type": "string" 234 | }, 235 | "owner": { 236 | "type": "object" 237 | }, 238 | "active": { 239 | "type": "object" 240 | }, 241 | "posting": { 242 | "type": "object" 243 | }, 244 | "extensions": { 245 | "type": "array", 246 | "defaultValue": [] 247 | } 248 | } 249 | }, 250 | "vote": { 251 | "name": "Vote", 252 | "authority": "posting", 253 | "schema": { 254 | "voter": { 255 | "type": "account", 256 | "defaultValue": "__signer" 257 | }, 258 | "author": { 259 | "type": "account" 260 | }, 261 | "permlink": { 262 | "type": "string" 263 | }, 264 | "weight": { 265 | "type": "int", 266 | "defaultValue": 10000 267 | } 268 | } 269 | }, 270 | "limit_order_create": { 271 | "name": "Create limit order", 272 | "authority": "active", 273 | "schema": { 274 | "owner": { 275 | "type": "account", 276 | "defaultValue": "__signer" 277 | }, 278 | "orderid": { 279 | "type": "int" 280 | }, 281 | "amount_to_sell": { 282 | "type": "amount" 283 | }, 284 | "min_to_receive": { 285 | "type": "amount" 286 | }, 287 | "fill_or_kill": { 288 | "type": "bool" 289 | }, 290 | "expiration": { 291 | "type": "time" 292 | } 293 | } 294 | }, 295 | "limit_order_create2": { 296 | "name": "Create limit order", 297 | "authority": "active", 298 | "schema": { 299 | "owner": { 300 | "type": "account", 301 | "defaultValue": "__signer" 302 | }, 303 | "orderid": { 304 | "type": "int" 305 | }, 306 | "amount_to_sell": { 307 | "type": "amount" 308 | }, 309 | "exchange_rate": { 310 | "type": "object" 311 | }, 312 | "fill_or_kill": { 313 | "type": "bool" 314 | }, 315 | "expiration": { 316 | "type": "time" 317 | } 318 | } 319 | }, 320 | "limit_order_cancel": { 321 | "name": "Cancel limit order", 322 | "authority": "active", 323 | "schema": { 324 | "owner": { 325 | "type": "account", 326 | "defaultValue": "__signer" 327 | }, 328 | "orderid": { 329 | "type": "int" 330 | } 331 | } 332 | }, 333 | "claim_reward_balance": { 334 | "name": "Redeem rewards", 335 | "authority": "posting", 336 | "schema": { 337 | "account": { 338 | "type": "account", 339 | "defaultValue": "__signer" 340 | }, 341 | "reward_hive": { 342 | "type": "amount" 343 | }, 344 | "reward_hbd": { 345 | "type": "amount" 346 | }, 347 | "reward_vests": { 348 | "type": "amount" 349 | } 350 | } 351 | }, 352 | "comment": { 353 | "name": "Post or comment", 354 | "authority": "posting", 355 | "schema": { 356 | "parent_author": { 357 | "type": "account", 358 | "defaultValue": "" 359 | }, 360 | "parent_permlink": { 361 | "type": "string" 362 | }, 363 | "author": { 364 | "type": "account", 365 | "defaultValue": "__signer" 366 | }, 367 | "permlink": { 368 | "type": "string" 369 | }, 370 | "title": { 371 | "type": "string" 372 | }, 373 | "body": { 374 | "type": "string" 375 | }, 376 | "json_metadata": { 377 | "type": "string" 378 | } 379 | } 380 | }, 381 | "comment_options": { 382 | "name": "Post or comment options", 383 | "authority": "posting", 384 | "schema": { 385 | "author": { 386 | "type": "account", 387 | "defaultValue": "__signer" 388 | }, 389 | "permlink": { 390 | "type": "string" 391 | }, 392 | "allow_curation_rewards": { 393 | "type": "bool", 394 | "defaultValue": true 395 | }, 396 | "allow_votes": { 397 | "type": "bool", 398 | "defaultValue": true 399 | }, 400 | "max_accepted_payout": { 401 | "type": "amount", 402 | "defaultValue": "1000000.000 SBD" 403 | }, 404 | "percent_hbd": { 405 | "type": "int", 406 | "defaultValue": 10000 407 | }, 408 | "extensions": { 409 | "type": "array", 410 | "defaultValue": [] 411 | } 412 | } 413 | }, 414 | "custom_json": { 415 | "name": "Custom operation", 416 | "authority": "posting", 417 | "schema": { 418 | "required_auths": { 419 | "type": "array", 420 | "defaultValue": [] 421 | }, 422 | "required_posting_auths": { 423 | "name": "posting auths", 424 | "type": "array", 425 | "defaultValue": ["__signer"] 426 | }, 427 | "id": { 428 | "type": "string" 429 | }, 430 | "json": { 431 | "type": "json" 432 | } 433 | } 434 | }, 435 | "delete_comment": { 436 | "name": "Delete comment", 437 | "authority": "posting", 438 | "schema": { 439 | "author": { 440 | "type": "account", 441 | "defaultValue": "__signer" 442 | }, 443 | "permlink": { 444 | "type": "string" 445 | } 446 | } 447 | }, 448 | "account_update": { 449 | "name": "Update account", 450 | "authority": "active", 451 | "schema": { 452 | "account": { 453 | "type": "account", 454 | "defaultValue": "__signer" 455 | }, 456 | "memo_key": { 457 | "type": "string" 458 | }, 459 | "json_metadata": { 460 | "type": "json" 461 | } 462 | } 463 | }, 464 | "account_update2": { 465 | "name": "Update account", 466 | "authority": "posting", 467 | "schema": { 468 | "account": { 469 | "type": "account", 470 | "defaultValue": "__signer" 471 | }, 472 | "json_metadata": { 473 | "type": "json", 474 | "defaultValue": "" 475 | }, 476 | "posting_json_metadata": { 477 | "type": "json" 478 | }, 479 | "extensions": { 480 | "type": "array", 481 | "defaultValue": [] 482 | } 483 | } 484 | }, 485 | "change_recovery_account": { 486 | "name": "Change recovery account", 487 | "authority": "owner", 488 | "schema": { 489 | "account_to_recover": { 490 | "type": "account", 491 | "defaultValue": "__signer" 492 | }, 493 | "new_recovery_account": { 494 | "type": "account" 495 | }, 496 | "extensions": { 497 | "type": "array", 498 | "defaultValue": [] 499 | } 500 | } 501 | }, 502 | "create_proposal": { 503 | "name": "Create proposal", 504 | "authority": "active", 505 | "schema": { 506 | "creator": { 507 | "type": "account", 508 | "defaultValue": "__signer" 509 | }, 510 | "receiver": { 511 | "type": "account", 512 | "defaultValue": "__signer" 513 | }, 514 | "start_date": { 515 | "type": "time" 516 | }, 517 | "end_date": { 518 | "type": "time" 519 | }, 520 | "daily_pay": { 521 | "type": "amount" 522 | }, 523 | "subject": { 524 | "type": "string" 525 | }, 526 | "permlink": { 527 | "type": "string" 528 | }, 529 | "extensions": { 530 | "type": "array", 531 | "defaultValue": [] 532 | } 533 | } 534 | }, 535 | "remove_proposal": { 536 | "name": "Remove proposal", 537 | "authority": "active", 538 | "schema": { 539 | "proposal_owner": { 540 | "type": "account", 541 | "defaultValue": "__signer" 542 | }, 543 | "proposal_ids": { 544 | "type": "array" 545 | }, 546 | "extensions": { 547 | "type": "array", 548 | "defaultValue": [] 549 | } 550 | } 551 | }, 552 | "update_proposal_votes": { 553 | "name": "Update proposal votes", 554 | "authority": "active", 555 | "schema": { 556 | "voter": { 557 | "type": "account", 558 | "defaultValue": "__signer" 559 | }, 560 | "proposal_ids": { 561 | "type": "array" 562 | }, 563 | "approve": { 564 | "type": "bool", 565 | "defaultValue": true 566 | }, 567 | "extensions": { 568 | "type": "array", 569 | "defaultValue": [] 570 | } 571 | } 572 | } 573 | } 574 | -------------------------------------------------------------------------------- /src/views/Login.vue: -------------------------------------------------------------------------------- 1 | 114 | 115 | 476 | -------------------------------------------------------------------------------- /src/views/Import.vue: -------------------------------------------------------------------------------- 1 | 167 | 168 | 553 | --------------------------------------------------------------------------------