├── src
├── assets
│ ├── .gitkeep
│ ├── logo.png
│ └── logo.svg
├── util
│ ├── iroha
│ │ ├── index.js
│ │ ├── util.js
│ │ ├── functions.js
│ │ ├── queries.js
│ │ └── commands.js
│ ├── date-format.js
│ ├── iroha-amount.js
│ ├── store-util.js
│ ├── storage-util.js
│ └── iroha-util.js
├── store.js
├── store
│ ├── index.js
│ └── Account.js
├── main.js
├── components
│ ├── Transactions.vue
│ ├── SummaryPage.vue
│ ├── WalletsPage.vue
│ ├── Dashboard.vue
│ ├── Wallet.vue
│ ├── TransferForm.vue
│ └── Login.vue
├── router.js
└── App.vue
├── scripts
├── admin@iroha.priv
├── alice@iroha.priv
├── setup.js
├── wallets.json
└── setup-accounts-and-assets.js
├── docker
├── iroha
│ ├── keys
│ │ ├── node0.priv
│ │ ├── node0.pub
│ │ ├── admin@iroha.priv
│ │ └── admin@iroha.pub
│ ├── entrypoint.sh
│ ├── config.docker
│ └── genesis.block
├── README.md
├── docker-compose-dev.yaml
├── docker-compose.yaml
└── grpcwebproxy
│ └── Dockerfile
├── public
├── favicon.ico
└── index.html
├── tests
└── unit
│ └── .eslintrc.js
├── .gitignore
├── .eslintrc.js
├── .babelrc
├── vue.config.js
├── README.md
├── package.json
└── LICENSE
/src/assets/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/scripts/admin@iroha.priv:
--------------------------------------------------------------------------------
1 | 0f0ce16d2afbb8eca23c7d8c2724f0c257a800ee2bbd54688cec6b898e3f7e33
--------------------------------------------------------------------------------
/scripts/alice@iroha.priv:
--------------------------------------------------------------------------------
1 | 9c430dfe8c54b0a447e25f75121119ac3b649c1253bce8420f245e4c104dccd1
--------------------------------------------------------------------------------
/docker/iroha/keys/node0.priv:
--------------------------------------------------------------------------------
1 | 41209bd907789fd5a796ac6bdff908bac2f7abcf7a1d0b99a18290f285f6e965
2 |
--------------------------------------------------------------------------------
/docker/iroha/keys/node0.pub:
--------------------------------------------------------------------------------
1 | d04da271b57fe63426ae1dc97f6952104037411fcf4f3b739dc217f45e5fc99b
2 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/soramitsu/iroha-wallet-js/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/docker/iroha/keys/admin@iroha.priv:
--------------------------------------------------------------------------------
1 | 0f0ce16d2afbb8eca23c7d8c2724f0c257a800ee2bbd54688cec6b898e3f7e33
2 |
--------------------------------------------------------------------------------
/docker/iroha/keys/admin@iroha.pub:
--------------------------------------------------------------------------------
1 | 889f6b881e331be21487db77dcf32c5f8d3d5e8066e78d2feac4239fe91d416f
2 |
--------------------------------------------------------------------------------
/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/soramitsu/iroha-wallet-js/HEAD/src/assets/logo.png
--------------------------------------------------------------------------------
/docker/README.md:
--------------------------------------------------------------------------------
1 | ### How to run?
2 |
3 | ``` docker-compose up --build ```
4 |
5 | ### How to kill?
6 |
7 | ``` docker-compose down ```
8 |
9 |
--------------------------------------------------------------------------------
/tests/unit/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | mocha: true
4 | },
5 | rules: {
6 | 'import/no-extraneous-dependencies': 'off'
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/docker/iroha/entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | #while ! curl http://wallet-js-iroha-postgres:5432/ 2>&1 | grep '52'
3 | #do
4 | #done
5 | sleep 10
6 | irohad --genesis_block genesis.block --config config.docker --keypair_name $KEY --overwrite-ledger
7 |
--------------------------------------------------------------------------------
/src/util/iroha/index.js:
--------------------------------------------------------------------------------
1 | import * as queries from './queries'
2 | import * as commands from './commands'
3 | import * as functions from './functions'
4 |
5 | export default {
6 | ...queries,
7 | ...commands,
8 | ...functions
9 | }
10 |
--------------------------------------------------------------------------------
/src/store.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Vuex from 'vuex'
3 |
4 | import modules from './store/index.js'
5 |
6 | Vue.use(Vuex)
7 |
8 | export default new Vuex.Store({
9 | modules,
10 | strict: process.env.NODE_ENV !== 'production'
11 | })
12 |
--------------------------------------------------------------------------------
/scripts/setup.js:
--------------------------------------------------------------------------------
1 | /*
2 | * NODE_IP=http://localhost:8080 DEBUG=iroha-util node scripts/setup.js
3 | */
4 | require('@babel/polyfill')
5 | require('@babel/register')({
6 | presets: [ '@babel/env' ]
7 | })
8 |
9 | module.exports = require('./setup-accounts-and-assets')
10 |
--------------------------------------------------------------------------------
/src/util/date-format.js:
--------------------------------------------------------------------------------
1 | import { format } from 'date-fns'
2 |
3 | const dateFormat = {
4 | filters: {
5 | formatDate: date => format(date, 'MMM. D, HH:mm'),
6 | formatDateLong: date => format(date, 'MMMM D, YYYY HH:mm:ss')
7 | }
8 | }
9 |
10 | export default dateFormat
11 | export const filters = dateFormat.filters
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist
4 | /dist_electron
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 |
--------------------------------------------------------------------------------
/src/util/iroha-amount.js:
--------------------------------------------------------------------------------
1 | /*
2 | * modify an amount object to a string like '123.45'
3 | */
4 | export function amountToString ({ value, precision }) {
5 | // TODO: use all values from 'first' to 'fourth'
6 | return String(value.fourth)
7 | .padStart(precision, '0')
8 | .replace(RegExp(`(\\d{${precision}})$`), '.$1')
9 | .replace(/^\./, '0.')
10 | .replace(/\.$/, '')
11 | }
12 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: {
4 | node: true
5 | },
6 | 'extends': [
7 | 'plugin:vue/essential',
8 | '@vue/standard'
9 | ],
10 | rules: {
11 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
12 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
13 | },
14 | parserOptions: {
15 | parser: 'babel-eslint'
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/store/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * The file enables `@/store/index.js` to import all vuex modules
3 | * in a one-shot manner. There should not be any reason to edit this file.
4 | */
5 |
6 | const files = require.context('.', false, /\.js$/)
7 | const modules = {}
8 |
9 | files.keys().forEach(key => {
10 | if (key === './index.js') return
11 | modules[key.replace(/(\.\/|\.js)/g, '')] = files(key).default
12 | })
13 |
14 | export default modules
15 |
--------------------------------------------------------------------------------
/docker/iroha/config.docker:
--------------------------------------------------------------------------------
1 | {
2 | "block_store_path" : "/tmp/block_store/",
3 | "torii_port" : 50051,
4 | "internal_port" : 10001,
5 | "pg_opt" : "host=wallet-js-iroha-postgres port=5432 user=postgres password=mysecretpassword",
6 | "max_proposal_size" : 1000,
7 | "proposal_delay" : 1000,
8 | "vote_delay" : 1000,
9 | "load_delay" : 5000,
10 | "mst_enable" : true,
11 | "mst_expiration_time" : 1440,
12 | "max_rounds_delay": 10,
13 | "stale_stream_max_rounds": 100000
14 | }
15 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "@babel/preset-env",
5 | {
6 | "targets": {
7 | "node": 8
8 | },
9 | "useBuiltIns": "entry",
10 | "exclude": ["transform-regenerator"]
11 | }
12 | ]
13 | ],
14 | "plugins": [
15 | [
16 | "component",
17 | {
18 | "libraryName": "element-ui",
19 | "styleLibraryName": "theme-chalk"
20 | }
21 | ],
22 | "@babel/plugin-syntax-dynamic-import"
23 | ]
24 | }
--------------------------------------------------------------------------------
/scripts/wallets.json:
--------------------------------------------------------------------------------
1 | {
2 | "wallets": [
3 | {
4 | "name": "AAAAA",
5 | "asset": "AAA",
6 | "amount": "224.38",
7 | "precision": 18
8 | },
9 | {
10 | "name": "DDDDD",
11 | "asset": "DDD",
12 | "amount": "2423.74",
13 | "precision": 18
14 | },
15 | {
16 | "name": "CCCCC",
17 | "asset": "CCC",
18 | "amount": "11.958",
19 | "precision": 18
20 | },
21 | {
22 | "name": "BBBBB",
23 | "asset": "BBB",
24 | "amount": "176.49",
25 | "precision": 18
26 | }
27 | ]
28 | }
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Iroha Wallet
9 |
10 |
11 |
12 | We're sorry but Iroha Wallet doesn't work properly without JavaScript enabled. Please enable it to continue.
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/vue.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 |
3 | const rules = []
4 |
5 | if (process.env.NODE_ENV === 'test') {
6 | rules.push({
7 | test: /\.(js|ts)$/,
8 | include: path.resolve('src'),
9 | loader: 'istanbul-instrumenter-loader',
10 | query: {
11 | esModules: true
12 | }
13 | })
14 | }
15 |
16 | module.exports = {
17 | chainWebpack: config => {
18 | config.resolve.alias
19 | .set('@util', path.resolve(__dirname, 'src/util'))
20 | .set('@router', path.resolve(__dirname, 'src/router.js'))
21 | },
22 | configureWebpack: {
23 | module: {
24 | rules: rules
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import '@babel/polyfill'
2 |
3 | import Vue from 'vue'
4 | import App from './App.vue'
5 | import router from './router'
6 | import store from './store.js'
7 |
8 | import { library } from '@fortawesome/fontawesome-svg-core'
9 | import { faTachometerAlt, faWallet, faSignOutAlt, faUpload } from '@fortawesome/free-solid-svg-icons'
10 | import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
11 |
12 | import ElementUI from 'element-ui'
13 | import locale from 'element-ui/lib/locale/lang/en'
14 |
15 | Vue.use(ElementUI, { locale })
16 |
17 | library.add(faTachometerAlt, faWallet, faSignOutAlt, faUpload)
18 |
19 | Vue.component('fa-icon', FontAwesomeIcon)
20 |
21 | Vue.config.productionTip = false
22 |
23 | new Vue({
24 | router,
25 | store,
26 | render: h => h(App)
27 | }).$mount('#app')
28 |
--------------------------------------------------------------------------------
/src/components/Transactions.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | {{ scope.row.date | formatDate }}
16 |
17 |
18 |
19 |
20 |
21 |
22 |
42 |
43 |
45 |
--------------------------------------------------------------------------------
/src/router.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Router from 'vue-router'
3 |
4 | Vue.use(Router)
5 |
6 | // TODO: Add route guards https://router.vuejs.org/en/advanced/navigation-guards.html
7 | export default new Router({
8 | mode: 'hash',
9 | routes: [
10 | {
11 | path: '/',
12 | redirect: '/login'
13 | },
14 | {
15 | path: '/login',
16 | name: 'login',
17 | component: require('@/components/Login').default
18 | },
19 | {
20 | path: '/dashboard',
21 | name: 'dashboard',
22 | component: require('@/components/Dashboard').default,
23 | children: [
24 | {
25 | path: 'summary-page',
26 | name: 'summary-page',
27 | component: require('@/components/SummaryPage').default
28 | },
29 | {
30 | path: 'wallets-page',
31 | name: 'wallets-page',
32 | component: require('@/components/WalletsPage').default,
33 | children: [
34 | {
35 | path: ':walletId',
36 | name: 'wallet',
37 | component: require('@/components/Wallet').default
38 | }
39 | ]
40 | }
41 | ]
42 | },
43 | {
44 | path: '*',
45 | redirect: '/'
46 | }
47 | ]
48 | })
49 |
--------------------------------------------------------------------------------
/docker/docker-compose-dev.yaml:
--------------------------------------------------------------------------------
1 | version: '3.5'
2 |
3 | networks:
4 | wallet-js:
5 | name: wallet-js-${SUBNET}
6 | attachable: true
7 |
8 | services:
9 | wallet-js-iroha:
10 | image: hyperledger/iroha:1.0.0_rc4-hotfix1
11 | container_name: wallet-js-iroha-${SUBNET}
12 | depends_on:
13 | - wallet-js-iroha-postgres
14 | tty: true
15 | environment:
16 | - KEY=keys/node0
17 | entrypoint:
18 | - /opt/iroha_data/entrypoint.sh
19 | networks:
20 | - wallet-js
21 | volumes:
22 | - ./iroha:/opt/iroha_data
23 | ports:
24 | - 50051:50051
25 |
26 | wallet-js-iroha-postgres:
27 | image: postgres:9.5
28 | container_name: wallet-js-iroha-postgres-${SUBNET}
29 | environment:
30 | - POSTGRES_PASSWORD=mysecretpassword
31 | networks:
32 | - wallet-js
33 | logging:
34 | driver: none
35 |
36 | grpcwebproxy:
37 | build:
38 | context: grpcwebproxy/
39 | container_name: wallet-js-grpcwebproxy-${SUBNET}
40 | depends_on:
41 | - wallet-js-iroha
42 | entrypoint:
43 | - grpcwebproxy
44 | - --backend_addr=wallet-js-iroha:50051
45 | - --run_tls_server=false
46 | - --allow_all_origins=true
47 | networks:
48 | - wallet-js
49 | logging:
50 | driver: none
51 | ports:
52 | - 8081:8080
53 |
--------------------------------------------------------------------------------
/docker/docker-compose.yaml:
--------------------------------------------------------------------------------
1 | version: '3.5'
2 |
3 | networks:
4 | wallet-js:
5 | name: wallet-js-${SUBNET}
6 | attachable: true
7 |
8 | services:
9 | wallet-js-iroha:
10 | image: hyperledger/iroha:1.0.0_rc4-hotfix1
11 | container_name: wallet-js-iroha-${SUBNET}
12 | depends_on:
13 | - wallet-js-iroha-postgres
14 | tty: true
15 | environment:
16 | - KEY=keys/node0
17 | entrypoint:
18 | - /opt/iroha_data/entrypoint.sh
19 | networks:
20 | - wallet-js
21 | volumes:
22 | - ./iroha:/opt/iroha_data
23 | logging:
24 | driver: none
25 | ports:
26 | - 50051:50051
27 |
28 | wallet-js-iroha-postgres:
29 | image: postgres:9.5
30 | container_name: wallet-js-iroha-postgres-${SUBNET}
31 | environment:
32 | - POSTGRES_PASSWORD=mysecretpassword
33 | networks:
34 | - wallet-js
35 | logging:
36 | driver: none
37 |
38 | grpcwebproxy:
39 | build:
40 | context: grpcwebproxy/
41 | container_name: wallet-js-grpcwebproxy-${SUBNET}
42 | depends_on:
43 | - wallet-js-iroha
44 | entrypoint:
45 | - grpcwebproxy
46 | - --backend_addr=wallet-js-iroha:50051
47 | - --run_tls_server=false
48 | - --allow_all_origins=true
49 | networks:
50 | - wallet-js
51 | logging:
52 | driver: none
53 | ports:
54 | - 8081:8080
55 |
--------------------------------------------------------------------------------
/src/util/iroha/util.js:
--------------------------------------------------------------------------------
1 | import {
2 | CommandService_v1Client as CommandService,
3 | QueryService_v1Client as QueryService
4 | } from 'iroha-helpers/lib/proto/endpoint_pb_service'
5 |
6 | export const DEFAULT_TIMEOUT_LIMIT = 5000
7 |
8 | /**
9 | * cached items available from start to end of the app
10 | * plus, `nodeIp` is persisted by localStorage
11 | */
12 | export const cache = {
13 | username: null, // NOT persisted by localStorage
14 | // TODO: do not cache keys; require a private key on every action instead.
15 | // For now it is needed for queries until tokens are implemented.
16 | key: null, // NOT persisted by localStorage
17 | nodeIp: null // persisted by localStorage
18 | }
19 |
20 | export function newCommandService () {
21 | return new CommandService(cache.nodeIp)
22 | }
23 |
24 | export function newQueryService () {
25 | return new QueryService(cache.nodeIp)
26 | }
27 |
28 | export function newCommandServiceOptions (privateKeys, quorum) {
29 | return {
30 | privateKeys,
31 | quorum,
32 | creatorAccountId: cache.username,
33 | commandService: new CommandService(cache.nodeIp),
34 | timeoutLimit: DEFAULT_TIMEOUT_LIMIT
35 | }
36 | }
37 |
38 | export function newQueryServiceOptions () {
39 | return {
40 | privateKey: cache.key,
41 | creatorAccountId: cache.username,
42 | queryService: new QueryService(cache.nodeIp),
43 | timeoutLimit: DEFAULT_TIMEOUT_LIMIT
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/util/iroha/functions.js:
--------------------------------------------------------------------------------
1 | import { cryptoHelper } from 'iroha-helpers'
2 | import { getAccount } from './queries'
3 | import { cache } from './util'
4 | import { getItem, setItem, removeItem } from '../storage-util'
5 | import Debug from 'debug'
6 | const debug = Debug('iroha-util')
7 |
8 | /**
9 | * login
10 | * @param {String} username
11 | * @param {String} privateKey length is 64
12 | * @param {String} nodeIp
13 | */
14 | function login (username, privateKey, nodeIp) {
15 | debug('starting login...')
16 |
17 | if (privateKey.length !== 64) {
18 | return Promise.reject(new Error('privateKey should have length of 64'))
19 | }
20 |
21 | cache.username = username
22 | cache.key = privateKey
23 | cache.nodeIp = nodeIp
24 |
25 | setItem('iroha-wallet:nodeIp', nodeIp)
26 |
27 | return getAccount({
28 | accountId: username
29 | })
30 | .then(account => {
31 | debug('login succeeded!')
32 | return account
33 | })
34 | .catch(err => {
35 | debug('login failed')
36 | throw err
37 | })
38 | }
39 |
40 | function logout () {
41 | cache.username = null
42 | cache.key = null
43 | cache.nodeIp = null
44 |
45 | return Promise.resolve()
46 | }
47 |
48 | function getStoredNodeIp () {
49 | return getItem('iroha-wallet:nodeIp') || ''
50 | }
51 |
52 | function clearStorage () {
53 | removeItem('iroha-wallet:nodeIp')
54 | }
55 |
56 | function isLoggedIn () {
57 | return !!cache.username
58 | }
59 |
60 | // generate new keypair
61 | const generateKeypair = cryptoHelper.generateKeyPair
62 |
63 | export {
64 | login,
65 | logout,
66 | isLoggedIn,
67 | clearStorage,
68 | getStoredNodeIp,
69 | generateKeypair
70 | }
71 |
--------------------------------------------------------------------------------
/src/util/iroha/queries.js:
--------------------------------------------------------------------------------
1 | import { queries } from 'iroha-helpers'
2 | import { newQueryServiceOptions } from './util'
3 |
4 | const getAccount = ({
5 | accountId
6 | }) => queries.getAccount(
7 | newQueryServiceOptions(), {
8 | accountId
9 | }
10 | )
11 |
12 | const getAccountAssets = ({
13 | accountId
14 | }) => queries.getAccountAssets(
15 | newQueryServiceOptions(), {
16 | accountId
17 | }
18 | )
19 |
20 | const getAccountDetail = ({
21 | accountId,
22 | key,
23 | writer
24 | }) => queries.getAccountDetail(
25 | newQueryServiceOptions(), {
26 | accountId,
27 | key,
28 | writer
29 | }
30 | )
31 |
32 | const getAccountTransactions = ({
33 | accountId,
34 | pageSize,
35 | firstTxHash
36 | }) => queries.getAccountTransactions(
37 | newQueryServiceOptions(), {
38 | accountId,
39 | pageSize,
40 | firstTxHash
41 | }
42 | )
43 |
44 | const getAccountAssetTransactions = ({
45 | accountId,
46 | assetId,
47 | pageSize,
48 | firstTxHash
49 | }) => queries.getAccountAssetTransactions(
50 | newQueryServiceOptions(), {
51 | accountId,
52 | assetId,
53 | pageSize,
54 | firstTxHash
55 | }
56 | )
57 |
58 | const getSignatories = ({
59 | accountId
60 | }) => queries.getSignatories(
61 | newQueryServiceOptions(), {
62 | accountId
63 | }
64 | )
65 |
66 | const getRawPendingTransactions = () => queries.getRawPendingTransactions(
67 | newQueryServiceOptions()
68 | )
69 |
70 | const getAssetInfo = ({
71 | assetId
72 | }) => queries.getAssetInfo(
73 | newQueryServiceOptions(), {
74 | assetId
75 | }
76 | )
77 |
78 | export {
79 | getAccount,
80 | getAccountAssets,
81 | getAccountAssetTransactions,
82 | getAccountTransactions,
83 | getAccountDetail,
84 | getSignatories,
85 | getRawPendingTransactions,
86 | getAssetInfo
87 | }
88 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
31 |
32 |
91 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Iroha Wallet
2 |
3 |
4 | [](https://www.npmjs.com/package/iroha-helpers)
5 | [](https://github.com/hyperledger/iroha/releases/tag/1.0.0_rc5)
6 | [](https://standardjs.com)
7 | [](https://opensource.org/licenses/Apache-2.0)
8 |
9 | Iroha Wallet is an example wallet application for [Iroha](http://iroha.readthedocs.io/).
10 |
11 | Iroha Wallet has following features:
12 |
13 | 1. Login with your Iroha account.
14 | 2. See your assets and transactions.
15 | 3. Transfer your assets to another Iroha account.
16 |
17 | 
18 |
19 | ## Getting Started
20 |
21 | ### Prerequisites
22 |
23 | First, you need to have an Iroha instance working. You can read how to launch it on [Iroha's docs](http://iroha.readthedocs.io/en/latest/getting_started/index.html). In this guide we assume the local Iroha instance working at `localhost:50051`.
24 |
25 | Also we provide docker that you can easily run and use.
26 | ```bash
27 | docker-compose -f docker/docker-compose.yaml up
28 | ```
29 |
30 | Then, populate the database by our example script as below. The script will create new account `alice@test` and several new assets. Note that there already exist `admin@test`. Their keys are in `scripts/`.
31 | Note that you need to complete "Installation" steps in advance if you want to use the example script.
32 |
33 | ```
34 | % node scripts/setup.js
35 | ```
36 |
37 | ### Installation
38 |
39 | Install npm packages.
40 |
41 | ```bash
42 | # install dependencies
43 | yarn install
44 | ```
45 |
46 | ### Running
47 |
48 | To run application
49 |
50 | ```bash
51 | yarn serve
52 | ```
--------------------------------------------------------------------------------
/src/components/SummaryPage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Account: {{ accountId }}
6 | Please auth!
7 |
8 | {{ key }}: {{ value }}
9 |
10 |
11 |
12 |
13 |
14 | {{ wallet.name }} {{ wallet.amount }}
15 |
16 | No assets
17 |
18 |
19 |
20 |
21 |
22 | Transactions history
23 |
24 |
25 |
26 |
27 |
28 |
29 |
70 |
71 |
76 |
--------------------------------------------------------------------------------
/src/util/store-util.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash'
2 |
3 | export function getTransferAssetsFrom (transactions, accountId, settlements = []) {
4 | if (_.isEmpty(transactions)) return []
5 |
6 | const transformed = []
7 |
8 | transactions.forEach(t => {
9 | const { commandsList, createdTime } = t.payload.reducedPayload
10 |
11 | commandsList.forEach(c => {
12 | if (!c.transferAsset) return
13 |
14 | const {
15 | amount,
16 | assetId,
17 | destAccountId,
18 | srcAccountId,
19 | description
20 | } = c.transferAsset
21 |
22 | const tx = {
23 | from: srcAccountId === accountId ? 'you' : srcAccountId,
24 | to: destAccountId === accountId ? 'you' : destAccountId,
25 | amount: amount,
26 | date: createdTime,
27 | currency: assetId,
28 | message: description
29 | }
30 |
31 | transformed.push(tx)
32 | })
33 | })
34 |
35 | /*
36 | * As actions.getAccountTransactions() does, we fetch account's txs
37 | * by multiple getAccount*Asset*Transactions calls.
38 | *
39 | * Also, getAccount*Asset*Transactions returns txs each of which includes
40 | * one or more command(s), which possibly includes also commands issued
41 | * against different asset.
42 | *
43 | * Therefore, when merging transactions for multiple assets, duplication
44 | * possibly occurs.
45 | * e.g.
46 | * accountAssetTransactions_of_asset_A = [
47 | * { commands: [command_for_asset_A_1, command_for_asset_B_1] },
48 | * { commands: [command_for_asset_A_2] }
49 | * ]
50 | * accountAssetTransactions_of_asset_B = [
51 | * { commands: [command_for_asset_A_1, command_for_asset_B_1] }
52 | * ]
53 | * // -> command_for_asset_A_1 and B_1 duplicates!
54 | *
55 | * To avoid it, we uniq the transactions.
56 | */
57 | return _(transformed)
58 | .chain()
59 | .uniqWith(_.isEqual)
60 | .sortBy('date')
61 | .reverse()
62 | .value()
63 | }
64 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "iroha-wallet-js",
3 | "productName": "Iroha Wallet",
4 | "version": "0.2.0",
5 | "author": "Keiji Yahata ",
6 | "contributors": [
7 | "Vyacheslav Bikbaev ",
8 | "Robert Sayakhov "
9 | ],
10 | "description": "An example wallet application for Iroha",
11 | "license": "SEE LICENSE IN LICENSE",
12 | "scripts": {
13 | "serve": "vue-cli-service serve --open",
14 | "build": "vue-cli-service build",
15 | "lint": "vue-cli-service lint",
16 | "test:unit": "vue-cli-service test:unit",
17 | "test:e2e": "vue-cli-service test:e2e"
18 | },
19 | "dependencies": {
20 | "@fortawesome/fontawesome-svg-core": "^1.2.15",
21 | "@fortawesome/free-solid-svg-icons": "^5.7.2",
22 | "@fortawesome/vue-fontawesome": "^0.1.6",
23 | "babel-plugin-component": "^1.1.1",
24 | "date-fns": "^1.30.1",
25 | "element-ui": "2.6.1",
26 | "iroha-helpers": "0.6.4",
27 | "lodash": "^4.17.19",
28 | "source-map-support": "^0.5.11",
29 | "vue": "^2.6.9",
30 | "vue-router": "^3.0.2",
31 | "vuex": "^3.1.0"
32 | },
33 | "devDependencies": {
34 | "@babel/core": "^7.0.0",
35 | "@babel/plugin-syntax-dynamic-import": "^7.0.0",
36 | "@babel/polyfill": "^7.2.5",
37 | "@babel/preset-env": "^7.3.4",
38 | "@babel/register": "^7.0.0",
39 | "@vue/cli-plugin-babel": "3.5.1",
40 | "@vue/cli-plugin-eslint": "3.5.1",
41 | "@vue/cli-plugin-unit-mocha": "3.5.1",
42 | "@vue/cli-service": "3.5.1",
43 | "@vue/eslint-config-standard": "4.0.0",
44 | "@vue/test-utils": "1.0.0-beta.29",
45 | "chai": "^4.2.0",
46 | "grpc": "^1.19.0",
47 | "lint-staged": "^7.2.2",
48 | "node-sass": "^4.13.1",
49 | "sass-loader": "^7.1.0",
50 | "vue-template-compiler": "^2.6.9"
51 | },
52 | "browserslist": [
53 | "> 1%",
54 | "last 2 versions",
55 | "not ie <= 8"
56 | ],
57 | "gitHooks": {
58 | "pre-commit": "lint-staged"
59 | },
60 | "lint-staged": {
61 | "*.js": [
62 | "vue-cli-service lint",
63 | "git add"
64 | ],
65 | "*.vue": [
66 | "vue-cli-service lint",
67 | "git add"
68 | ]
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/util/iroha/commands.js:
--------------------------------------------------------------------------------
1 | import {
2 | commands
3 | } from 'iroha-helpers'
4 |
5 | import {
6 | newCommandServiceOptions
7 | } from './util'
8 |
9 | const createAccount = (privateKeys, quorum, {
10 | accountName,
11 | domainId,
12 | publicKey
13 | }) => commands.createAccount(
14 | newCommandServiceOptions(privateKeys, quorum), {
15 | accountName,
16 | domainId,
17 | publicKey
18 | }
19 | )
20 |
21 | const createAsset = (privateKeys, quorum, {
22 | assetName,
23 | domainId,
24 | precision
25 | }) => commands.createAsset(
26 | newCommandServiceOptions(privateKeys, quorum), {
27 | assetName,
28 | domainId,
29 | precision
30 | }
31 | )
32 |
33 | const transferAsset = (privateKeys, quorum, {
34 | srcAccountId,
35 | destAccountId,
36 | assetId,
37 | description,
38 | amount
39 | }) => commands.transferAsset(
40 | newCommandServiceOptions(privateKeys, quorum), {
41 | srcAccountId,
42 | destAccountId,
43 | assetId,
44 | description,
45 | amount
46 | }
47 | )
48 |
49 | const addSignatory = (privateKeys, quorum, {
50 | accountId,
51 | publicKey
52 | }) => commands.addSignatory(
53 | newCommandServiceOptions(privateKeys, quorum), {
54 | accountId,
55 | publicKey
56 | }
57 | )
58 |
59 | const removeSignatory = (privateKeys, quorum, {
60 | accountId,
61 | publicKey
62 | }) => commands.removeSignatory(
63 | newCommandServiceOptions(privateKeys, quorum), {
64 | accountId,
65 | publicKey
66 | }
67 | )
68 |
69 | const addAssetQuantity = (privateKeys, quorum, {
70 | assetId,
71 | amount
72 | }) => commands.addAssetQuantity(
73 | newCommandServiceOptions(privateKeys, quorum), {
74 | assetId,
75 | amount
76 | }
77 | )
78 |
79 | const setAccountDetail = (privateKeys, quorum, {
80 | accountId,
81 | key,
82 | value
83 | }) => commands.setAccountDetail(
84 | newCommandServiceOptions(privateKeys, quorum), {
85 | accountId,
86 | key,
87 | value
88 | }
89 | )
90 |
91 | const setAccountQuorum = (privateKeys, currentQuorum, {
92 | accountId,
93 | quorum
94 | }) => commands.setAccountQuorum(
95 | newCommandServiceOptions(privateKeys, currentQuorum), {
96 | accountId,
97 | quorum
98 | }
99 | )
100 |
101 | export {
102 | createAccount,
103 | createAsset,
104 | transferAsset,
105 | addSignatory,
106 | removeSignatory,
107 | addAssetQuantity,
108 | setAccountDetail,
109 | setAccountQuorum
110 | }
111 |
--------------------------------------------------------------------------------
/docker/grpcwebproxy/Dockerfile:
--------------------------------------------------------------------------------
1 | # Use the `golang` image to build a statically linked binary.
2 | # https://blog.codeship.com/building-minimal-docker-containers-for-go-applications/
3 | #
4 | # Tested with:
5 | # golang@sha256:4826b5c314a498142c7291ad835ab6be1bf02f7813d6932d01f1f0f1383cdda1
6 | FROM golang as gobin
7 |
8 | # Clone the improbable-eng/grpc-web repo.
9 | WORKDIR /go/src/github.com/improbable-eng
10 | RUN git clone https://github.com/improbable-eng/grpc-web.git
11 |
12 | # Use `dep` to ensure the correct versions of vendor dependencies are used.
13 | # See: https://github.com/improbable-eng/grpc-web/issues/174
14 | WORKDIR /go/src/github.com/improbable-eng/grpc-web
15 | RUN go get github.com/golang/dep/cmd/dep
16 | RUN dep ensure
17 |
18 | # Build a static binary for `grpcwebproxy`.
19 | WORKDIR /go/src/github.com/improbable-eng/grpc-web/go/grpcwebproxy
20 | ENV CGO_ENABLED='0' GOOS='linux'
21 | RUN go install
22 |
23 | # Use the `alpine` image to get `ca-certificates`.
24 | #
25 | # Tested with:
26 | # alpine@sha256:7df6db5aa61ae9480f52f0b3a06a140ab98d427f86d8d5de0bedab9b8df6b1c0
27 | FROM alpine as certs
28 | RUN apk update
29 | RUN apk add ca-certificates
30 |
31 | # Build the image from the `scratch` (empty) container by copying the binary
32 | # and SSL certificates into an approapriate location.
33 | FROM scratch
34 | COPY --from=gobin ["/go/bin/grpcwebproxy", "/bin/"]
35 | COPY --from=gobin ["/go/src/github.com/improbable-eng/grpc-web/misc/localhost.*", "/misc/"]
36 | COPY --from=certs ["/etc/ssl/*", "/etc/ssl/"]
37 |
38 | # Advertise ports 8080 and 8443.
39 | EXPOSE 8080 8443
40 |
41 | # Start the `grpcwebproxy` binary as the main process.
42 | ENTRYPOINT ["/bin/grpcwebproxy"]
43 |
44 | # Provide default arguments for `grpcwebproxy`. These will normally be
45 | # overridden when running the container. Using ENV for HOST and PORT would
46 | # be better here, but ENV variables don't expand without a shell.
47 | #
48 | # See: https://github.com/moby/moby/issues/5509#issuecomment-42173047
49 | #
50 | # Instead, `dev.localdomain` is used for the host. This can be set when
51 | # running the container by using the argument:
52 | #
53 | # `--add-host dev.localdomain:192.0.2.1
54 | #
55 | # Replace 192.0.2.1 with the IP address of the host.
56 | #
57 | # Port 50051 is used because it's the most common port used in the GRPC
58 | # quickstart examples.
59 | CMD ["--server_tls_cert_file=/misc/localhost.crt", \
60 | "--server_tls_key_file=/misc/localhost.key", \
61 | "--backend_addr=dev.localdomain:50051", \
62 | "--backend_tls_noverify"]
63 |
--------------------------------------------------------------------------------
/docker/iroha/genesis.block:
--------------------------------------------------------------------------------
1 | {
2 | "blockV1": {
3 | "payload": {
4 | "transactions": [
5 | {
6 | "payload": {
7 | "reducedPayload": {
8 | "commands": [
9 | {
10 | "addPeer": {
11 | "peer": {
12 | "address": "localhost:10001",
13 | "peerKey": "d04da271b57fe63426ae1dc97f6952104037411fcf4f3b739dc217f45e5fc99b"
14 | }
15 | }
16 | },
17 | {
18 | "createRole": {
19 | "roleName": "tester",
20 | "permissions": [
21 | "can_create_asset",
22 | "can_set_detail",
23 | "can_transfer",
24 | "can_receive",
25 | "can_add_asset_qty",
26 | "can_subtract_asset_qty",
27 | "can_create_domain",
28 | "can_grant_can_add_my_signatory",
29 | "can_grant_can_remove_my_signatory",
30 | "can_add_peer",
31 | "can_append_role",
32 | "can_create_role",
33 | "can_detach_role",
34 | "can_get_my_acc_ast_txs",
35 | "can_get_all_acc_detail",
36 | "can_get_all_accounts",
37 | "can_get_all_acc_ast",
38 | "can_get_blocks",
39 | "can_get_roles",
40 | "can_get_all_signatories",
41 | "can_get_domain_accounts",
42 | "can_set_quorum",
43 | "can_add_signatory",
44 | "can_create_account",
45 | "can_get_all_txs",
46 | "can_get_domain_acc_detail",
47 | "can_read_assets",
48 | "can_grant_can_set_my_quorum",
49 | "can_grant_can_transfer_my_assets",
50 | "can_get_my_signatories",
51 | "can_remove_signatory"
52 | ]
53 | }
54 | },
55 | {
56 | "createDomain": {
57 | "domainId": "iroha",
58 | "defaultRole": "tester"
59 | }
60 | },
61 | {
62 | "createAccount": {
63 | "accountName": "admin",
64 | "domainId": "iroha",
65 | "publicKey": "889f6b881e331be21487db77dcf32c5f8d3d5e8066e78d2feac4239fe91d416f"
66 | }
67 | }
68 | ],
69 | "creatorAccountId": "admin@iroha",
70 | "quorum": 1
71 | }
72 | }
73 | }
74 | ],
75 | "txNumber": 1,
76 | "height": "1",
77 | "prevBlockHash": "0000000000000000000000000000000000000000000000000000000000000000"
78 | }
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/components/WalletsPage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
17 |
18 |
19 |
20 |
21 |
22 | No assets
23 |
24 |
25 | Please generate assets with setup script!
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
83 |
84 |
127 |
--------------------------------------------------------------------------------
/src/components/Dashboard.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
68 |
69 |
133 |
--------------------------------------------------------------------------------
/src/util/storage-util.js:
--------------------------------------------------------------------------------
1 | import set from 'lodash/fp/set'
2 |
3 | const localStorage = global.localStorage || {
4 | setItem () {},
5 | getItem () {},
6 | removeItem () {}
7 | }
8 |
9 | /**
10 | * getItem() wrapper of the native getItem() method of the Storage interface,
11 | * when passed a key name, will return that key's value or null if the key does not exist.
12 | * @param {String} key Key of value that you want to get from local storage.
13 | * @return {(String|null)} String representation of value or null if the key does not exist.
14 | */
15 | const getItem = (key) => {
16 | return localStorage.getItem(key)
17 | }
18 |
19 | /**
20 | * getParsedItem() wrapper of the native getItem() method of the Storage interface,
21 | * when passed a key name, will return that key's parsed value or null if the key does not exist.
22 | * @param {String} key Key of value that you want to get from local storage.
23 | * @returns {(Object|null)} Object representation of value or null if the key does not exist.
24 | */
25 | const getParsedItem = (key) => {
26 | return JSON.parse(localStorage.getItem(key))
27 | }
28 |
29 | /**
30 | * setItem() wrapper of the native setItem() method of Storage interface,
31 | * when passed a key name and value, will add that key to the storage,
32 | * or update that key's value if it already exists.
33 | * @param {String} key Key that you want to create / update.
34 | * @param {Any} value Value that will be stored / updated for the key.
35 | */
36 | const setItem = (key, value) => {
37 | localStorage.setItem(key, value)
38 | }
39 |
40 | /**
41 | * setStringifyItem() wrapper of the native setItem() method of Storage interface,
42 | * when passed a key name and value, will add that key to the storage,
43 | * or update that key's value if it already exists.
44 | * The value will be stringifyed.
45 | * @param {String} key Key that you want to create / update.
46 | * @param {Any} value Value that will be stored / updated for the key.
47 | */
48 | const setStringifyItem = (key, value) => {
49 | localStorage.setItem(key, JSON.stringify(value))
50 | }
51 |
52 | /**
53 | * setParsedItem() wrapper of the native setItem() method of Storage interface,
54 | * when passed a key name and value, will add that key to the storage,
55 | * or update that key's value if it already exists.
56 | * @param {String} key Key that you want to create / update. Key also can be a path of the property to set.
57 | * @param {Any} value Value that will be stored / updated for the key.
58 | */
59 | const setParsedItem = (key, value) => {
60 | const path = key.split('.')
61 | const i = getParsedItem(path[0])
62 | const v = set(path.slice(1))(value)(i)
63 | setStringifyItem(path[0], v)
64 | }
65 |
66 | /**
67 | * removeItem() wrapper of the native removeItem() method of Storage interface,
68 | * when passed a key name and value, will add that key to the storage,
69 | * or update that key's value if it already exists.
70 | * @param {String} key Key that you want to create / update.
71 | * @param {Any} value Value that will be stored / updated for the key.
72 | */
73 | const removeItem = (key, value) => {
74 | localStorage.removeItem(key, value)
75 | }
76 |
77 | export {
78 | getItem,
79 | getParsedItem,
80 | setItem,
81 | setStringifyItem,
82 | setParsedItem,
83 | removeItem
84 | }
85 |
--------------------------------------------------------------------------------
/src/components/Wallet.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
16 |
17 |
18 |
19 |
20 |
21 |
114 |
115 |
117 |
--------------------------------------------------------------------------------
/src/assets/logo.svg:
--------------------------------------------------------------------------------
1 | Asset 1
--------------------------------------------------------------------------------
/src/components/TransferForm.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
31 |
32 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
54 | SEND
55 |
56 |
57 |
58 |
59 |
60 |
61 |
138 |
139 |
148 |
--------------------------------------------------------------------------------
/src/components/Login.vue:
--------------------------------------------------------------------------------
1 |
2 |
60 |
61 |
62 |
137 |
138 |
160 |
--------------------------------------------------------------------------------
/scripts/setup-accounts-and-assets.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-vars */
2 | /* eslint-disable no-return-await */
3 | import fs from 'fs'
4 | import path from 'path'
5 | import _ from 'lodash'
6 | import {
7 | derivePublicKey
8 | } from 'ed25519.js'
9 |
10 | import grpc from 'grpc'
11 | import {
12 | QueryService_v1Client as QueryService,
13 | CommandService_v1Client as CommandService
14 | } from 'iroha-helpers/lib/proto/endpoint_grpc_pb'
15 | import { commands, queries } from 'iroha-helpers'
16 |
17 | const irohaDomain = 'iroha'
18 | const testAccName = 'admin'
19 | const aliceAccName = 'alice'
20 | const testAccFull = `${testAccName}@${irohaDomain}`
21 | const aliceAccFull = `${aliceAccName}@${irohaDomain}`
22 |
23 | const testPrivKeyHex = fs.readFileSync(path.join(__dirname, `${testAccFull}.priv`)).toString().trim()
24 | const testPubKey = derivePublicKey(Buffer.from(testPrivKeyHex, 'hex')).toString('hex')
25 | const alicePrivKeyHex = fs.readFileSync(path.join(__dirname, `${aliceAccFull}.priv`)).toString().trim()
26 | const alicePubKey = derivePublicKey(Buffer.from(alicePrivKeyHex, 'hex')).toString('hex')
27 |
28 | const NODE_IP = process.env.NODE_IP || 'localhost:50051'
29 | const DEFAULT_TIMEOUT_LIMIT = 5000
30 | const DUMMY_FILE_PATH = path.join(__dirname, 'wallets.json')
31 | const accounts = [testAccFull, aliceAccFull]
32 | const wallets = require(DUMMY_FILE_PATH).wallets
33 |
34 | console.log('\x1b[33m%s\x1b[0m', '#### INFO ####')
35 | console.log(`setting up accounts and assets with using '${DUMMY_FILE_PATH}'`)
36 | console.log(`accounts: ${accounts.join(', ')}`)
37 | console.log(`assets: ${wallets.map(w => w.name).join(', ')}`)
38 | console.log('\x1b[33m%s\x1b[0m', '#### INFO ####\n')
39 |
40 | function newCommandServiceOptions (privateKeys, quorum, accountId) {
41 | return {
42 | privateKeys,
43 | quorum,
44 | creatorAccountId: accountId,
45 | commandService: new CommandService(
46 | NODE_IP,
47 | grpc.credentials.createInsecure()
48 | ),
49 | timeoutLimit: DEFAULT_TIMEOUT_LIMIT
50 | }
51 | }
52 |
53 | function newQueryServiceOptions (privateKey, accountId) {
54 | return {
55 | privateKey: privateKey,
56 | creatorAccountId: accountId,
57 | queryService: new QueryService(
58 | NODE_IP,
59 | grpc.credentials.createInsecure()
60 | ),
61 | timeoutLimit: DEFAULT_TIMEOUT_LIMIT
62 | }
63 | }
64 |
65 | new Promise((resolve, reject) => resolve())
66 | .then(async () => await tryToCreateAccount(aliceAccName, irohaDomain, alicePubKey))
67 | .then(async () => setup())
68 | .then((res) => console.log(res))
69 | .then(() => console.log('### DONE ###'))
70 | .catch((err) => console.log(err))
71 |
72 | async function setup () {
73 | await initializeAssets()
74 | }
75 |
76 | async function initializeAssets () {
77 | console.log('initializing assets')
78 |
79 | for (let w of wallets) {
80 | const precision = w.precision
81 | const amount = w.amount
82 | const assetName = w.name.toLowerCase()
83 | const assetId = assetName + `#${irohaDomain}`
84 |
85 | console.log('\x1b[36m%s\x1b[0m', `#### ${assetName} BEGIN ####`)
86 |
87 | await tryToCreateAsset(assetName, irohaDomain, precision)
88 | await tryAddAssetQuantity(assetId, amount)
89 | await tryToSplitAmount(assetId, amount)
90 | await tryToSendRandomAmount(assetId, testAccFull, amount, precision, [testPrivKeyHex], 1)
91 | await tryToSendRandomAmount(assetId, aliceAccFull, amount, precision, [alicePrivKeyHex], 1)
92 |
93 | console.log('\x1b[36m%s\x1b[0m', `#### ${assetName} END ####`)
94 | }
95 | }
96 |
97 | async function tryToCreateAccount (accountName, domainId, publicKey) {
98 | console.log(`trying to create an account: ${accountName}@${domainId}`)
99 | try {
100 | await commands.createAccount(
101 | newCommandServiceOptions([testPrivKeyHex], 1, testAccFull),
102 | {
103 | accountName,
104 | domainId,
105 | publicKey
106 | }
107 | )
108 | console.log(`${accountName}@${domainId} has successfully been created`)
109 | } catch (error) {
110 | const accountId = `${accountName}@${domainId}`
111 | try {
112 | const account = await queries.getAccount(
113 | newQueryServiceOptions(testPrivKeyHex, testAccFull),
114 | {
115 | accountId
116 | }
117 | )
118 | console.log(`${account.accountId} already exist`)
119 | } catch (error) {
120 | console.log(error)
121 | }
122 | }
123 | }
124 |
125 | async function tryToCreateAsset (assetName, domainId, precision) {
126 | console.log(`trying to create an asset: ${assetName}#${domainId} (precision=${precision})`)
127 | try {
128 | await commands.createAsset(
129 | newCommandServiceOptions([testPrivKeyHex], 1, testAccFull),
130 | {
131 | assetName,
132 | domainId,
133 | precision
134 | }
135 | )
136 | console.log(`${assetName}#${domainId} (precision: ${precision}) has successfully been created`)
137 | } catch (error) {
138 | try {
139 | const assetId = `${assetName}#${domainId}`
140 | const info = await queries.getAssetInfo(
141 | newQueryServiceOptions(testPrivKeyHex, testAccFull),
142 | {
143 | assetId
144 | }
145 | )
146 | if (info.precision === precision) {
147 | console.log(`${assetName}#${domainId} (precision=${precision}) already exist`)
148 | } else {
149 | console.log(`${assetName}#${domainId} is already used with different precision`)
150 | }
151 | } catch (error) {
152 | console.log(error)
153 | }
154 | }
155 | }
156 |
157 | async function tryAddAssetQuantity (assetId, amount) {
158 | console.log(`adding initial amount of ${assetId} to ${testAccFull}`)
159 | try {
160 | await commands.addAssetQuantity(
161 | newCommandServiceOptions([testPrivKeyHex], 1, testAccFull),
162 | {
163 | assetId,
164 | amount
165 | }
166 | )
167 | } catch (error) {
168 | console.log(`Error! Asset quantity not added ${assetId} ${amount}`)
169 | }
170 | }
171 |
172 | async function tryToSplitAmount (assetId, amount) {
173 | const splittedAmount = String(Math.round(amount * 0.3))
174 | console.log(`transfer 1/3 ${splittedAmount} initial amount of ${assetId} to ${aliceAccFull}`)
175 | try {
176 | await commands.transferAsset(
177 | newCommandServiceOptions([testPrivKeyHex], 1, testAccFull),
178 | {
179 | srcAccountId: testAccFull,
180 | destAccountId: aliceAccFull,
181 | assetId,
182 | description: 'transfer 1/3',
183 | amount: splittedAmount
184 | }
185 | )
186 | } catch (error) {
187 | console.log(`Error! Can't transfer 1/3 to ${aliceAccFull}`)
188 | }
189 | }
190 |
191 | async function tryToSendRandomAmount (assetId, accountId, amount, precision, privateKeys, quorum) {
192 | console.log(`Sending random amount (${amount}) of ${assetId} to ${accountId}`)
193 | const from = accountId
194 | const to = _.sample(_.without(accounts, from))
195 | const txAmount = _.random(3, 5)
196 | for (let i = 0; i < txAmount; i++) {
197 | const message = _.sample(['Deal #1', 'Deal #2', 'Deal #3', 'PART_OF_DUMMY_SETTLEMENT'])
198 | const amount = String(Math.random()).substr(0, precision + 2)
199 | try {
200 | await commands.transferAsset(
201 | newCommandServiceOptions(privateKeys, quorum, accountId),
202 | {
203 | srcAccountId: from,
204 | destAccountId: to,
205 | assetId,
206 | description: message,
207 | amount: amount
208 | }
209 | )
210 | } catch (error) {
211 | console.log(error)
212 | }
213 | }
214 | }
215 |
--------------------------------------------------------------------------------
/src/store/Account.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import _ from 'lodash'
3 | import irohaUtil from '@util/iroha'
4 | import { getTransferAssetsFrom } from '@util/store-util'
5 |
6 | const types = _([
7 | 'SIGNUP',
8 | 'LOGIN',
9 | 'LOGOUT',
10 | 'GET_ACCOUNT_TRANSACTIONS',
11 | 'GET_ACCOUNT_ASSET_TRANSACTIONS',
12 | 'GET_ALL_ACCOUNT_ASSETS_TRANSACTIONS',
13 | 'GET_ACCOUNT_ASSETS',
14 | 'GET_ALL_UNSIGNED_TRANSACTIONS',
15 | 'TRANSFER_ASSET',
16 | 'GET_ACCOUNT_QUORUM'
17 | ]).chain()
18 | .flatMap(x => [x + '_REQUEST', x + '_SUCCESS', x + '_FAILURE'])
19 | .concat(['RESET'])
20 | .map(x => [x, x])
21 | .fromPairs()
22 | .value()
23 |
24 | function initialState () {
25 | return {
26 | accountId: '',
27 | nodeIp: irohaUtil.getStoredNodeIp(),
28 | accountInfo: {},
29 | rawAssetTransactions: {},
30 | rawUnsignedTransactions: [],
31 | rawTransactions: [],
32 | assets: [],
33 | connectionError: null,
34 | accountQuorum: 0
35 | }
36 | }
37 |
38 | const state = initialState()
39 |
40 | const getters = {
41 | transfers (state) {
42 | const txs = Object.values(state.rawAssetTransactions)
43 | .map(a => a.transactionsList)
44 | return getTransferAssetsFrom(_.flatten(txs), state.accountId)
45 | },
46 |
47 | wallets (state) {
48 | return state.assets.map(a => {
49 | return {
50 | id: a.assetId.replace(/#/g, '$'),
51 | name: a.assetId,
52 | amount: a.balance,
53 | precision: a.balance.precision
54 | }
55 | })
56 | },
57 |
58 | getTransactionsByAssetId: (state) => (assetId) => {
59 | return state.rawAssetTransactions[assetId] ? getTransferAssetsFrom(
60 | state.rawAssetTransactions[assetId].transactionsList,
61 | state.accountId
62 | ) : []
63 | }
64 | }
65 |
66 | /**
67 | * Store a connection error so the top component can handle it.
68 | * @param {Object} state
69 | * @param {Error} err
70 | */
71 | function handleError (state, err) {
72 | console.error(err)
73 | throw err
74 | }
75 |
76 | const mutations = {
77 | [types.RESET] (state) {
78 | const s = initialState()
79 |
80 | Object.keys(s).forEach(key => {
81 | state[key] = s[key]
82 | })
83 | },
84 |
85 | [types.SIGNUP_REQUEST] (state) {},
86 |
87 | [types.SIGNUP_SUCCESS] (state, params) {
88 | },
89 |
90 | [types.SIGNUP_FAILURE] (state, err) {
91 | handleError(state, err)
92 | },
93 |
94 | [types.LOGIN_REQUEST] (state) {},
95 |
96 | [types.LOGIN_SUCCESS] (state, account) {
97 | state.accountId = account.accountId
98 | },
99 |
100 | [types.LOGIN_FAILURE] (state, err) {
101 | handleError(state, err)
102 | },
103 |
104 | [types.LOGOUT_REQUEST] (state) {},
105 |
106 | [types.LOGOUT_SUCCESS] (state) {},
107 |
108 | [types.LOGOUT_FAILURE] (state, err) {
109 | handleError(state, err)
110 | },
111 |
112 | [types.GET_ACCOUNT_ASSET_TRANSACTIONS_REQUEST] (state) {},
113 |
114 | [types.GET_ACCOUNT_ASSET_TRANSACTIONS_SUCCESS] (state, { assetId, transactions }) {
115 | Vue.set(state.rawAssetTransactions, assetId, transactions)
116 | },
117 |
118 | [types.GET_ACCOUNT_ASSET_TRANSACTIONS_FAILURE] (state, err) {
119 | handleError(state, err)
120 | },
121 |
122 | [types.GET_ALL_ACCOUNT_ASSETS_TRANSACTIONS_REQUEST] (state) {},
123 |
124 | [types.GET_ALL_ACCOUNT_ASSETS_TRANSACTIONS_SUCCESS] (state) {},
125 |
126 | [types.GET_ALL_ACCOUNT_ASSETS_TRANSACTIONS_FAILURE] (state, err) {
127 | handleError(state, err)
128 | },
129 |
130 | [types.GET_ACCOUNT_ASSETS_REQUEST] (state) {},
131 |
132 | [types.GET_ACCOUNT_ASSETS_SUCCESS] (state, assets) {
133 | state.assets = assets
134 | },
135 |
136 | [types.GET_ACCOUNT_ASSETS_FAILURE] (state, err) {
137 | handleError(state, err)
138 | },
139 |
140 | [types.GET_ACCOUNT_TRANSACTIONS_REQUEST] (state) {},
141 |
142 | [types.GET_ACCOUNT_TRANSACTIONS_SUCCESS] (state, transactions) {
143 | state.rawTransactions = transactions
144 | },
145 |
146 | [types.GET_ACCOUNT_TRANSACTIONS_FAILURE] (state, err) {
147 | handleError(state, err)
148 | },
149 |
150 | [types.GET_ALL_UNSIGNED_TRANSACTIONS_REQUEST] (state) {},
151 |
152 | [types.GET_ALL_UNSIGNED_TRANSACTIONS_SUCCESS] (state, transactions) {
153 | state.rawUnsignedTransactions = transactions
154 | },
155 |
156 | [types.GET_ALL_UNSIGNED_TRANSACTIONS_FAILURE] (state, err) {
157 | handleError(state, err)
158 | },
159 |
160 | [types.TRANSFER_ASSET_REQUEST] (state) {},
161 |
162 | [types.TRANSFER_ASSET_SUCCESS] (state) {},
163 |
164 | [types.TRANSFER_ASSET_FAILURE] (state, err) {
165 | handleError(state, err)
166 | },
167 |
168 | [types.GET_ACCOUNT_QUORUM_REQUEST] (state) {},
169 |
170 | [types.GET_ACCOUNT_QUORUM_SUCCESS] (state, { quorum }) {
171 | state.accountQuorum = quorum
172 | },
173 |
174 | [types.GET_ACCOUNT_QUORUM_FAILURE] (state, err) {
175 | handleError(state, err)
176 | }
177 | }
178 |
179 | const actions = {
180 | signup ({ commit }, { username }) {
181 | commit(types.SIGNUP_REQUEST)
182 |
183 | const { publicKey, privateKey } = irohaUtil.generateKeypair()
184 |
185 | // TODO: POST data to registration API
186 | return new Promise((resolve, reject) => {
187 | setTimeout(() => {
188 | console.log('signing up...')
189 | console.log('username:', username)
190 | console.log('publicKey:', publicKey)
191 |
192 | resolve()
193 | }, 1000)
194 | })
195 | .then(() => commit(types.SIGNUP_SUCCESS, { username, publicKey, privateKey }))
196 | .then(() => ({ username, privateKey }))
197 | .catch(err => {
198 | commit(types.SIGNUP_FAILURE, err)
199 | throw err
200 | })
201 | },
202 |
203 | login ({ commit }, { username, privateKey, nodeIp }) {
204 | commit(types.LOGIN_REQUEST)
205 |
206 | return irohaUtil.login(username, privateKey, nodeIp)
207 | .then(account => commit(types.LOGIN_SUCCESS, account))
208 | .catch(err => {
209 | commit(types.LOGIN_FAILURE, err)
210 | throw err
211 | })
212 | },
213 |
214 | logout ({ commit }) {
215 | commit(types.LOGOUT_REQUEST)
216 |
217 | return irohaUtil.logout()
218 | .then(() => {
219 | commit(types.RESET)
220 | commit(types.LOGOUT_SUCCESS)
221 | })
222 | .catch(err => {
223 | commit(types.LOGOUT_FAILURE, err)
224 | throw err
225 | })
226 | },
227 |
228 | getAccountAssetTransactions ({ commit, state }, { assetId }) {
229 | commit(types.GET_ACCOUNT_ASSET_TRANSACTIONS_REQUEST)
230 |
231 | return irohaUtil.getAccountAssetTransactions({
232 | accountId: state.accountId,
233 | assetId,
234 | pageSize: 100,
235 | firstTxHash: undefined
236 | })
237 | .then(responses => {
238 | commit(types.GET_ACCOUNT_ASSET_TRANSACTIONS_SUCCESS, {
239 | assetId: assetId,
240 | transactions: responses
241 | })
242 | })
243 | .catch(err => {
244 | commit(types.GET_ACCOUNT_ASSET_TRANSACTIONS_FAILURE, err)
245 | throw err
246 | })
247 | },
248 |
249 | getAllAccountAssetsTransactions ({ dispatch, commit, state }) {
250 | commit(types.GET_ALL_ACCOUNT_ASSETS_TRANSACTIONS_REQUEST)
251 |
252 | let gettingAccountAssets
253 |
254 | if (_.isEmpty(state.assets)) {
255 | gettingAccountAssets = dispatch('getAccountAssets')
256 | } else {
257 | gettingAccountAssets = Promise.resolve()
258 | }
259 |
260 | return gettingAccountAssets
261 | .then(() => {
262 | const gettingAllAccountAssetsTransactions = state.assets.map(a => {
263 | return dispatch('getAccountAssetTransactions', { assetId: a.assetId })
264 | })
265 |
266 | return Promise.all(gettingAllAccountAssetsTransactions)
267 | })
268 | .then(() => {
269 | commit(types.GET_ALL_ACCOUNT_ASSETS_TRANSACTIONS_SUCCESS)
270 | })
271 | .catch(err => {
272 | commit(types.GET_ALL_ACCOUNT_ASSETS_TRANSACTIONS_FAILURE, err)
273 | throw err
274 | })
275 | },
276 |
277 | getAccountAssets ({ commit, state }) {
278 | commit(types.GET_ACCOUNT_ASSETS_REQUEST)
279 |
280 | return irohaUtil.getAccountAssets({
281 | accountId: state.accountId
282 | })
283 | .then(assets => {
284 | commit(types.GET_ACCOUNT_ASSETS_SUCCESS, assets)
285 | })
286 | .catch(err => {
287 | commit(types.GET_ACCOUNT_ASSETS_FAILURE, err)
288 | throw err
289 | })
290 | },
291 |
292 | getAccountTransactions ({ commit, state }) {
293 | commit(types.GET_ACCOUNT_TRANSACTIONS_REQUEST)
294 |
295 | return irohaUtil.getAccountTransactions({
296 | accountId: state.accountId,
297 | pageSize: 100,
298 | firstTxHash: undefined
299 | })
300 | .then(transactions => {
301 | commit(types.GET_ACCOUNT_TRANSACTIONS_SUCCESS, transactions)
302 | })
303 | .catch(err => {
304 | commit(types.GET_ACCOUNT_TRANSACTIONS_FAILURE, err)
305 | throw err
306 | })
307 | },
308 |
309 | getAllUnsignedTransactions ({ commit, state }) {
310 | commit(types.GET_ALL_UNSIGNED_TRANSACTIONS_REQUEST)
311 |
312 | return irohaUtil.getRawPendingTransactions()
313 | .then(responses => {
314 | commit(types.GET_ALL_UNSIGNED_TRANSACTIONS_SUCCESS, responses)
315 | })
316 | .catch(err => {
317 | commit(types.GET_ALL_UNSIGNED_TRANSACTIONS_FAILURE, err)
318 | throw err
319 | })
320 | },
321 |
322 | transferAsset ({ commit, state }, { privateKeys, assetId, to, description = '', amount }) {
323 | commit(types.TRANSFER_ASSET_REQUEST)
324 | return irohaUtil.transferAsset(privateKeys, state.accountQuorum, {
325 | srcAccountId: state.accountId,
326 | destAccountId: to,
327 | assetId,
328 | description,
329 | amount
330 | })
331 | .then(() => {
332 | commit(types.TRANSFER_ASSET_SUCCESS)
333 | })
334 | .catch(err => {
335 | commit(types.TRANSFER_ASSET_FAILURE, err)
336 | throw err
337 | })
338 | },
339 |
340 | getAccountQuorum ({ commit, state }) {
341 | commit(types.GET_ACCOUNT_QUORUM_REQUEST)
342 | return irohaUtil.getAccount({
343 | accountId: state.accountId
344 | })
345 | .then((account) => commit(types.GET_ACCOUNT_QUORUM_SUCCESS, account))
346 | .catch(err => {
347 | commit(types.GET_ACCOUNT_QUORUM_FAILURE, err)
348 | throw err
349 | })
350 | }
351 | }
352 |
353 | export default {
354 | types,
355 | state,
356 | getters,
357 | mutations,
358 | actions
359 | }
360 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [2018] [Soramitsu Co., Ltd.]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/src/util/iroha-util.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-vars */
2 | const debug = require('debug')('iroha-util')
3 | const iroha = require('iroha-lib')
4 | const grpc = require('grpc')
5 |
6 | const endpointGrpc = require('iroha-lib/pb/endpoint_grpc_pb.js')
7 | const pbEndpoint = require('iroha-lib/pb/endpoint_pb.js')
8 | const pbResponse = require('iroha-lib/pb/qry_responses_pb')
9 |
10 | const txBuilder = new iroha.ModelTransactionBuilder()
11 | const queryBuilder = new iroha.ModelQueryBuilder()
12 | const crypto = new iroha.ModelCrypto()
13 |
14 | /**
15 | * default timeout limit of queries
16 | */
17 | const DEFAULT_TIMEOUT_LIMIT = 5000
18 |
19 | /**
20 | * cached items available from start to end of the app
21 | * plus, `nodeIp` is persisted by localStorage
22 | */
23 | const cache = {
24 | username: null, // NOT persisted by localStorage
25 | keys: null, // NOT persisted by localStorage
26 | nodeIp: null // persisted by localStorage
27 | }
28 |
29 | /**
30 | * mock localStorage for testing environment
31 | */
32 | const localStorage = global.localStorage || {
33 | setItem () {},
34 | getItem () {},
35 | removeItem () {}
36 | }
37 |
38 | /*
39 | * ===== functions =====
40 | */
41 | /**
42 | * login
43 | * @param {String} username
44 | * @param {String} privateKey length is 64
45 | * @param {String} nodeIp
46 | */
47 | function login (username, privateKey, nodeIp) {
48 | debug('starting login...')
49 |
50 | if (privateKey.length !== 64) {
51 | return Promise.reject(new Error('privateKey should have length of 64'))
52 | }
53 |
54 | const keys = crypto.convertFromExisting(
55 | crypto.fromPrivateKey(privateKey).publicKey().hex(),
56 | privateKey
57 | )
58 |
59 | cache.username = username
60 | cache.keys = keys
61 | cache.nodeIp = nodeIp
62 |
63 | localStorage.setItem('iroha-wallet:nodeIp', nodeIp)
64 |
65 | return getAccount(username)
66 | .then(account => {
67 | debug('login succeeded!')
68 | return account
69 | })
70 | .catch(err => {
71 | debug('login failed')
72 | throw err
73 | })
74 | }
75 |
76 | /**
77 | * clear local cache
78 | */
79 | function logout () {
80 | cache.username = null
81 | cache.keys = null
82 | cache.nodeIp = null
83 |
84 | return Promise.resolve()
85 | }
86 |
87 | /**
88 | * return node IP which was used before
89 | */
90 | function getStoredNodeIp () {
91 | return localStorage.getItem('iroha-wallet:nodeIp') || ''
92 | }
93 |
94 | /**
95 | * clear localStorage
96 | */
97 | function clearStorage () {
98 | localStorage.removeItem('iroha-wallet:nodeIp')
99 | }
100 |
101 | /**
102 | * return true if logged in
103 | */
104 | function isLoggedIn () {
105 | return !!cache.username
106 | }
107 |
108 | /**
109 | * generate new keypair
110 | */
111 | function generateKeypair () {
112 | const keypair = crypto.generateKeypair()
113 | const publicKey = keypair.publicKey().hex()
114 | const privateKey = keypair.privateKey().hex()
115 |
116 | return { publicKey, privateKey }
117 | }
118 |
119 | /*
120 | * ===== queries =====
121 | */
122 | /**
123 | * wrapper function of queries
124 | * @param {Function} buildQuery
125 | * @param {Function} onResponse
126 | * @param {Number} timeoutLimit timeoutLimit
127 | */
128 | function sendQuery (
129 | buildQuery = function () {},
130 | onResponse = function (resolve, reject, responseName, response) {},
131 | timeoutLimit = DEFAULT_TIMEOUT_LIMIT
132 | ) {
133 | return new Promise((resolve, reject) => {
134 | const queryClient = new endpointGrpc.QueryServiceClient(
135 | cache.nodeIp,
136 | grpc.credentials.createInsecure()
137 | )
138 | const query = buildQuery()
139 | const protoQuery = makeProtoQueryWithKeys(query, cache.keys)
140 |
141 | debug('submitting query...')
142 | debug('peer ip:', cache.nodeIp)
143 | debug('parameters:', JSON.stringify(protoQuery.toObject().payload, null, ' '))
144 | debug('')
145 |
146 | // grpc-node hangs against unresponsive server, which possibly occur when
147 | // invalid node IP is set. To avoid this problem, we use timeout timer.
148 | // c.f. https://github.com/grpc/grpc/issues/13163
149 | const timer = setTimeout(() => {
150 | queryClient.$channel.close()
151 | const err = new Error('please check IP address OR your internet connection')
152 | err.code = grpc.status.CANCELLED
153 | reject(err)
154 | }, timeoutLimit)
155 |
156 | queryClient.find(protoQuery, (err, response) => {
157 | clearTimeout(timer)
158 |
159 | if (err) {
160 | return reject(err)
161 | }
162 |
163 | debug('submitted query successfully!')
164 |
165 | const type = response.getResponseCase()
166 | const responseName = getProtoEnumName(
167 | pbResponse.QueryResponse.ResponseCase,
168 | 'iroha.protocol.QueryResponse',
169 | type
170 | )
171 |
172 | onResponse(resolve, reject, responseName, response)
173 | })
174 | })
175 | }
176 |
177 | /**
178 | * getAccount https://hyperledger.github.io/iroha-api/#get-account
179 | * @param {String} accountId
180 | */
181 | function getAccount (accountId) {
182 | debug('starting getAccount...')
183 |
184 | return sendQuery(
185 | () => {
186 | return queryBuilder
187 | .creatorAccountId(cache.username)
188 | .createdTime(Date.now())
189 | .queryCounter(1)
190 | .getAccount(accountId)
191 | .build()
192 | },
193 | (resolve, reject, responseName, response) => {
194 | if (responseName !== 'ACCOUNT_RESPONSE') {
195 | return reject(new Error(`Query response error: expected=ACCOUNT_RESPONSE, actual=${responseName}`))
196 | }
197 |
198 | const account = response.getAccountResponse().getAccount().toObject()
199 |
200 | debug('account', account)
201 |
202 | resolve(account)
203 | }
204 | )
205 | }
206 |
207 | /**
208 | * getAccountTransactions https://hyperledger.github.io/iroha-api/#get-account-transactions
209 | * @param {String} accountId
210 | */
211 | function getAccountTransactions (accountId) {
212 | debug('starting getAccountTransactions...')
213 |
214 | return sendQuery(
215 | () => {
216 | return queryBuilder
217 | .creatorAccountId(cache.username)
218 | .createdTime(Date.now())
219 | .queryCounter(1)
220 | .getAccountTransactions(accountId)
221 | .build()
222 | },
223 | (resolve, reject, responseName, response) => {
224 | if (responseName !== 'TRANSACTIONS_RESPONSE') {
225 | return reject(new Error(`Query response error: expected=TRANSACTIONS_RESPONSE, actual=${responseName}`))
226 | }
227 |
228 | const transactions = response.getTransactionsResponse().toObject().transactionsList
229 |
230 | debug('transactions', transactions)
231 |
232 | resolve(transactions)
233 | }
234 | )
235 | }
236 |
237 | /**
238 | * getAccountAssetTransactions https://hyperledger.github.io/iroha-api/#get-account-asset-transactions
239 | * @param {String} accountId
240 | * @param {String} assetId
241 | */
242 | function getAccountAssetTransactions (accountId, assetId) {
243 | debug('starting getAccountAssetTransactions...')
244 |
245 | return sendQuery(
246 | () => {
247 | return queryBuilder
248 | .creatorAccountId(cache.username)
249 | .createdTime(Date.now())
250 | .queryCounter(1)
251 | .getAccountAssetTransactions(accountId, assetId)
252 | .build()
253 | },
254 | (resolve, reject, responseName, response) => {
255 | if (responseName !== 'TRANSACTIONS_RESPONSE') {
256 | return reject(new Error(`Query response error: expected=TRANSACTIONS_RESPONSE, actual=${responseName}`))
257 | }
258 |
259 | const transactions = response.getTransactionsResponse().toObject().transactionsList
260 |
261 | debug('transactions', transactions)
262 |
263 | resolve(transactions)
264 | }
265 | )
266 | }
267 |
268 | /**
269 | * getAccountAssets https://hyperledger.github.io/iroha-api/#get-account-assets
270 | * @param {String} accountId
271 | */
272 | function getAccountAssets (accountId) {
273 | debug('starting getAccountAssets...')
274 |
275 | return sendQuery(
276 | () => {
277 | return queryBuilder
278 | .creatorAccountId(cache.username)
279 | .createdTime(Date.now())
280 | .queryCounter(1)
281 | .getAccountAssets(accountId)
282 | .build()
283 | },
284 | (resolve, reject, responseName, response) => {
285 | if (responseName !== 'ACCOUNT_ASSETS_RESPONSE') {
286 | return reject(new Error(`Query response error: expected=ACCOUNT_ASSETS_RESPONSE, actual=${responseName}`))
287 | }
288 |
289 | const assets = response.getAccountAssetsResponse().toObject().accountAssetsList
290 |
291 | debug('assets', assets)
292 |
293 | resolve(assets)
294 | }
295 | )
296 | }
297 |
298 | /**
299 | * getAssetInfo https://hyperledger.github.io/iroha-api/?protobuf#get-asset-info
300 | * @param {String} assetId
301 | */
302 | function getAssetInfo (assetId) {
303 | debug('starting getAssetInfo...')
304 |
305 | return sendQuery(
306 | () => {
307 | return queryBuilder
308 | .creatorAccountId(cache.username)
309 | .createdTime(Date.now())
310 | .queryCounter(1)
311 | .getAssetInfo(assetId)
312 | .build()
313 | },
314 | (resolve, reject, responseName, response) => {
315 | if (responseName !== 'ASSET_RESPONSE') {
316 | return reject(new Error(`Query response error: expected=ASSET_RESPONSE, actual=${responseName}`))
317 | }
318 |
319 | const info = response.getAssetResponse().toObject()
320 |
321 | debug('asset info', info)
322 |
323 | resolve(info)
324 | }
325 | )
326 | }
327 |
328 | // TODO: implement it
329 | function getAllUnsignedTransactions (accountId) {
330 | debug('starting getAllUnsignedTransactions...')
331 |
332 | return new Promise((resolve, reject) => {
333 | setTimeout(() => resolve(['DUMMY']), 500)
334 | })
335 | }
336 |
337 | /*
338 | * ===== commands =====
339 | */
340 | /**
341 | * wrapper function of commands
342 | * @param {Function} buildQuery
343 | * @param {Number} timeoutLimit timeoutLimit
344 | */
345 | function command (
346 | buildTx = function () {},
347 | timeoutLimit = DEFAULT_TIMEOUT_LIMIT
348 | ) {
349 | let txClient, txHash
350 |
351 | return new Promise((resolve, reject) => {
352 | const tx = buildTx()
353 | const protoTx = makeProtoTxWithKeys(tx, cache.keys)
354 |
355 | txClient = new endpointGrpc.CommandServiceClient(
356 | cache.nodeIp,
357 | grpc.credentials.createInsecure()
358 | )
359 | txHash = blob2array(tx.hash().blob())
360 |
361 | debug('submitting transaction...')
362 | debug('peer ip:', cache.nodeIp)
363 | debug('parameters:', JSON.stringify(protoTx.toObject().payload, null, ' '))
364 | debug('txhash:', Buffer.from(txHash).toString('hex'))
365 | debug('')
366 |
367 | const timer = setTimeout(() => {
368 | txClient.$channel.close()
369 | reject(new Error('please check IP address OR your internet connection'))
370 | }, timeoutLimit)
371 |
372 | txClient.torii(protoTx, (err, data) => {
373 | clearTimeout(timer)
374 |
375 | if (err) {
376 | return reject(err)
377 | }
378 |
379 | debug('submitted transaction successfully!')
380 | resolve()
381 | })
382 | })
383 | .then(() => {
384 | debug('sleep 5 seconds...')
385 | return sleep(5000)
386 | })
387 | .then(() => {
388 | debug('sending transaction status request...')
389 |
390 | return new Promise((resolve, reject) => {
391 | const request = new pbEndpoint.TxStatusRequest()
392 |
393 | request.setTxHash(txHash)
394 |
395 | txClient.status(request, (err, response) => {
396 | if (err) {
397 | return reject(err)
398 | }
399 |
400 | const status = response.getTxStatus()
401 | const TxStatus = require('iroha-lib/pb/endpoint_pb.js').TxStatus
402 | const statusName = getProtoEnumName(
403 | TxStatus,
404 | 'iroha.protocol.TxStatus',
405 | status
406 | )
407 |
408 | if (statusName !== 'COMMITTED') {
409 | return reject(new Error(`Your transaction wasn't commited: expected=COMMITED, actual=${statusName}`))
410 | }
411 |
412 | resolve()
413 | })
414 | })
415 | })
416 | }
417 |
418 | /**
419 | * createAccount https://hyperledger.github.io/iroha-api/?protobuf#create-account
420 | * @param {String} accountName
421 | * @param {String} domainId
422 | * @param {String} mainPubKey
423 | */
424 | function createAccount (accountName, domainId, mainPubKey) {
425 | debug('starting createAccount...')
426 |
427 | return command(
428 | function buildTx () {
429 | return txBuilder
430 | .creatorAccountId(cache.username)
431 | .createdTime(Date.now())
432 | .createAccount(accountName, domainId, mainPubKey)
433 | .build()
434 | }
435 | )
436 | }
437 |
438 | /**
439 | * createAsset https://hyperledger.github.io/iroha-api/#create-asset
440 | * @param {String} assetName
441 | * @param {String} domainI
442 | * @param {Number} precision
443 | */
444 | function createAsset (assetName, domainId, precision) {
445 | debug('starting createAsset...')
446 |
447 | return command(
448 | function buildTx () {
449 | return txBuilder
450 | .creatorAccountId(cache.username)
451 | .createdTime(Date.now())
452 | .createAsset(assetName, domainId, precision)
453 | .build()
454 | }
455 | )
456 | }
457 |
458 | /**
459 | * addAssetQuantity https://hyperledger.github.io/iroha-api/#add-asset-quantity
460 | * @param {String} assetId
461 | * @param {String} amount
462 | */
463 | function addAssetQuantity (assetId, amount) {
464 | debug('starting addAssetQuantity...')
465 |
466 | return command(
467 | function buildTx () {
468 | return txBuilder
469 | .creatorAccountId(cache.username)
470 | .createdTime(Date.now())
471 | .addAssetQuantity(assetId, amount)
472 | .build()
473 | }
474 | )
475 | }
476 |
477 | /**
478 | * transferAsset https://hyperledger.github.io/iroha-api/#transfer-asset
479 | * @param {String} srcAccountId
480 | * @param {String} destAccountId
481 | * @param {String} assetId
482 | * @param {String} description
483 | * @param {String} amount
484 | */
485 | function transferAsset (srcAccountId, destAccountId, assetId, description, amount) {
486 | debug('starting transferAsset...')
487 |
488 | return command(
489 | function () {
490 | return txBuilder
491 | .creatorAccountId(cache.username)
492 | .createdTime(Date.now())
493 | .transferAsset(srcAccountId, destAccountId, assetId, description, amount)
494 | .build()
495 | }
496 | )
497 | }
498 |
499 | /*
500 | * ===== utilities ===
501 | */
502 | function sleep (ms) {
503 | return new Promise(resolve => setTimeout(resolve, ms))
504 | }
505 |
506 | function blob2array (blob) {
507 | const bytearray = new Uint8Array(blob.size())
508 | for (let i = 0; i < blob.size(); ++i) {
509 | bytearray[i] = blob.get(i)
510 | }
511 | return bytearray
512 | }
513 |
514 | const protoEnumName = {}
515 | function getProtoEnumName (obj, key, value) {
516 | if (protoEnumName.hasOwnProperty(key)) {
517 | if (protoEnumName[key].length < value) {
518 | return 'unknown'
519 | } else {
520 | return protoEnumName[key][value]
521 | }
522 | } else {
523 | protoEnumName[key] = []
524 | for (var k in obj) {
525 | let idx = obj[k]
526 | if (isNaN(idx)) {
527 | debug(
528 | 'getProtoEnumName:wrong enum value, now is type of ' +
529 | typeof idx +
530 | ' should be integer'
531 | )
532 | } else {
533 | protoEnumName[key][idx] = k
534 | }
535 | }
536 | return getProtoEnumName(obj, key, value)
537 | }
538 | }
539 |
540 | function makeProtoQueryWithKeys (builtQuery, keys) {
541 | const pbQuery = require('iroha-lib/pb/queries_pb.js').Query
542 |
543 | const blob = new iroha.ModelProtoQuery(builtQuery).signAndAddSignature(keys).finish().blob()
544 | const arr = blob2array(blob)
545 | const protoQuery = pbQuery.deserializeBinary(arr)
546 |
547 | return protoQuery
548 | }
549 |
550 | function makeProtoTxWithKeys (builtTx, keys) {
551 | const pbTransaction = require('iroha-lib/pb/block_pb.js').Transaction
552 |
553 | const blob = new iroha.ModelProtoTransaction(builtTx).signAndAddSignature(keys).finish().blob()
554 | const arr = blob2array(blob)
555 | const protoTx = pbTransaction.deserializeBinary(arr)
556 |
557 | return protoTx
558 | }
559 |
560 | /*
561 | * ===== export ===
562 | */
563 | module.exports = {
564 | getStoredNodeIp,
565 | clearStorage,
566 | login,
567 | logout,
568 | isLoggedIn,
569 | generateKeypair,
570 |
571 | // queries
572 | getAccount,
573 | getAccountAssets,
574 | getAccountAssetTransactions,
575 | getAccountTransactions,
576 | getAssetInfo,
577 | getAllUnsignedTransactions,
578 |
579 | // commands
580 | createAccount,
581 | createAsset,
582 | transferAsset,
583 | addAssetQuantity
584 | }
585 |
--------------------------------------------------------------------------------