├── .gitignore ├── LICENSE ├── README.md ├── app-users ├── .babelrc ├── .editorconfig ├── .gitignore ├── .postcssrc.js ├── README.md ├── build │ ├── build.js │ ├── check-versions.js │ ├── logo.png │ ├── utils.js │ ├── vue-loader.conf.js │ ├── webpack.base.conf.js │ ├── webpack.dev.conf.js │ └── webpack.prod.conf.js ├── config │ ├── dev.env.js │ ├── index.js │ └── prod.env.js ├── index.html ├── package.json ├── src │ ├── App.vue │ ├── assets │ │ └── Users.json │ ├── components │ │ └── TopMenu.vue │ ├── libs │ │ ├── BcExplorer.js │ │ └── mixinViews.js │ ├── main.js │ ├── routes.js │ └── views │ │ ├── List.vue │ │ ├── Profile.vue │ │ └── Register.vue └── static │ └── .gitkeep ├── contracts ├── Migrations.sol └── Users.sol ├── migrations ├── 1_initial_migration.js └── 2_migrate_users.js ├── test └── users.js └── truffle.js /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /app-users/node_modules 3 | /app-users/package-lock.json 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Dan 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ethereum and VueJS DApp 2 | 3 | This is a simple decentralized application built using **Ethereum** blockchain and **Vue JS** for the front-end. This DApp example let the users store their profile (name and status) on the blockchain. 4 | 5 | ![N|Solid](https://www.danielefavi.com/wp-content/uploads/2018/05/ethereum_vue_dapp_user_list.png) 6 | 7 | # Brief description 8 | The DApp allow users to register their name and status on the blockchain. The user's profile is associated with an account address (or wallet addres). 9 | 10 | The owner of the profile is the only person who can modify his own data. This will give you a technical explanation of what it means when the user is in control of his own data (just like Facebook… sarcasm!). 11 | The **smart contract** is designed to give the user the **control of his own information** and not even the creator of the smart contract can control any data. 12 | 13 | # Prerequisite 14 | - NPM version 5.8.0 15 | - TRUFFLE verson 4.1.5 16 | - Ganache or your private network 17 | - Metamask: not mandatory but better if you want to register more users 18 | 19 | # Quick Installation 20 | 1) Download the project and decompress it into the folder **~/ethereum-vuejs-dapp** (or wherever you want). 21 | 2) Start ganache (or your private blockchain). 22 | 3) Open the terminal (if you are using Windows you **must use the Power Shell**) in the folder **~/ethereum-vuejs-dapp** and run the command: 23 | ```sh 24 | $ truffle console --network ganache 25 | ``` 26 | 4) If ganache is running you should be inside the truffle console; now run the following command in the truffle console: 27 | ```sh 28 | > migrate --reset --compile-all 29 | ``` 30 | 5) If the migration was successful, copy the file **~/ethereum-vuejs-dapp/build/contracts/Users.json** into the folder **~/ethereum-vuejs-dapp/app-users/src/assets/** 31 | 6) Open another terminal in the folder **~/ethereum-vuejs-dapp/app-users** and run the command: 32 | ```sh 33 | $ npm install 34 | ``` 35 | 7) Once all the dependencies are installed run the command: 36 | ```sh 37 | $ npm run dev 38 | ``` 39 | If everything went fine, the terminal will display a message similar to: 40 | ```sh 41 | DONE Compiled successfully in 5166ms 15:54:53 42 | 43 | Your application is running here: http://localhost:8080 44 | ``` 45 | 8) Open the browser, go to the URL shown by your terminal and play with the DApp! 46 | 47 | **NOTE:** if you want to try to add more user profiles you have to install metamask in your browser, then import the accounts into metamask and finally change account on metamask in order to register a new user. 48 | 49 | Visit [DanieleFavi.com](https://www.danielefavi.com/create-your-blockchain-dapp-with-ethereum-and-vuejs/) for more info. 50 | -------------------------------------------------------------------------------- /app-users/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "modules": false, 5 | "targets": { 6 | "browsers": ["> 1%", "last 2 versions", "not ie <= 8"] 7 | } 8 | }], 9 | "stage-2" 10 | ], 11 | "plugins": ["transform-vue-jsx", "transform-runtime"] 12 | } 13 | -------------------------------------------------------------------------------- /app-users/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /app-users/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | /dist/ 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Editor directories and files 9 | .idea 10 | .vscode 11 | *.suo 12 | *.ntvs* 13 | *.njsproj 14 | *.sln 15 | -------------------------------------------------------------------------------- /app-users/.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | "plugins": { 5 | "postcss-import": {}, 6 | "postcss-url": {}, 7 | // to edit target browsers: use "browserslist" field in package.json 8 | "autoprefixer": {} 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /app-users/README.md: -------------------------------------------------------------------------------- 1 | # Ethereum and VueJS dApp 2 | 3 | > **Ethereum** and **VueJS** dApp for managing user status on the **blockchain**. For more info please visit: 4 | https://www.danielefavi.com/create-your-blockchain-dapp-with-ethereum-and-vuejs/ 5 | 6 | ## Build Setup 7 | 8 | ``` bash 9 | # install dependencies 10 | npm install 11 | 12 | # serve with hot reload at localhost:8080 13 | npm run dev 14 | 15 | # build for production with minification 16 | npm run build 17 | 18 | # build for production and view the bundle analyzer report 19 | npm run build --report 20 | ``` 21 | 22 | For a detailed explanation on how things work, check out the [guide](http://vuejs-templates.github.io/webpack/) and [docs for vue-loader](http://vuejs.github.io/vue-loader). 23 | -------------------------------------------------------------------------------- /app-users/build/build.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | require('./check-versions')() 3 | 4 | process.env.NODE_ENV = 'production' 5 | 6 | const ora = require('ora') 7 | const rm = require('rimraf') 8 | const path = require('path') 9 | const chalk = require('chalk') 10 | const webpack = require('webpack') 11 | const config = require('../config') 12 | const webpackConfig = require('./webpack.prod.conf') 13 | 14 | const spinner = ora('building for production...') 15 | spinner.start() 16 | 17 | rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => { 18 | if (err) throw err 19 | webpack(webpackConfig, (err, stats) => { 20 | spinner.stop() 21 | if (err) throw err 22 | process.stdout.write(stats.toString({ 23 | colors: true, 24 | modules: false, 25 | children: false, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build. 26 | chunks: false, 27 | chunkModules: false 28 | }) + '\n\n') 29 | 30 | if (stats.hasErrors()) { 31 | console.log(chalk.red(' Build failed with errors.\n')) 32 | process.exit(1) 33 | } 34 | 35 | console.log(chalk.cyan(' Build complete.\n')) 36 | console.log(chalk.yellow( 37 | ' Tip: built files are meant to be served over an HTTP server.\n' + 38 | ' Opening index.html over file:// won\'t work.\n' 39 | )) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /app-users/build/check-versions.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const chalk = require('chalk') 3 | const semver = require('semver') 4 | const packageConfig = require('../package.json') 5 | const shell = require('shelljs') 6 | 7 | function exec (cmd) { 8 | return require('child_process').execSync(cmd).toString().trim() 9 | } 10 | 11 | const versionRequirements = [ 12 | { 13 | name: 'node', 14 | currentVersion: semver.clean(process.version), 15 | versionRequirement: packageConfig.engines.node 16 | } 17 | ] 18 | 19 | if (shell.which('npm')) { 20 | versionRequirements.push({ 21 | name: 'npm', 22 | currentVersion: exec('npm --version'), 23 | versionRequirement: packageConfig.engines.npm 24 | }) 25 | } 26 | 27 | module.exports = function () { 28 | const warnings = [] 29 | 30 | for (let i = 0; i < versionRequirements.length; i++) { 31 | const mod = versionRequirements[i] 32 | 33 | if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { 34 | warnings.push(mod.name + ': ' + 35 | chalk.red(mod.currentVersion) + ' should be ' + 36 | chalk.green(mod.versionRequirement) 37 | ) 38 | } 39 | } 40 | 41 | if (warnings.length) { 42 | console.log('') 43 | console.log(chalk.yellow('To use this template, you must update following to modules:')) 44 | console.log() 45 | 46 | for (let i = 0; i < warnings.length; i++) { 47 | const warning = warnings[i] 48 | console.log(' ' + warning) 49 | } 50 | 51 | console.log() 52 | process.exit(1) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app-users/build/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielefavi/ethereum-vuejs-dapp/e397cc0a0afc85671828926982f3788cbd60d62d/app-users/build/logo.png -------------------------------------------------------------------------------- /app-users/build/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const config = require('../config') 4 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 5 | const packageConfig = require('../package.json') 6 | 7 | exports.assetsPath = function (_path) { 8 | const assetsSubDirectory = process.env.NODE_ENV === 'production' 9 | ? config.build.assetsSubDirectory 10 | : config.dev.assetsSubDirectory 11 | 12 | return path.posix.join(assetsSubDirectory, _path) 13 | } 14 | 15 | exports.cssLoaders = function (options) { 16 | options = options || {} 17 | 18 | const cssLoader = { 19 | loader: 'css-loader', 20 | options: { 21 | sourceMap: options.sourceMap 22 | } 23 | } 24 | 25 | const postcssLoader = { 26 | loader: 'postcss-loader', 27 | options: { 28 | sourceMap: options.sourceMap 29 | } 30 | } 31 | 32 | // generate loader string to be used with extract text plugin 33 | function generateLoaders (loader, loaderOptions) { 34 | const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader] 35 | 36 | if (loader) { 37 | loaders.push({ 38 | loader: loader + '-loader', 39 | options: Object.assign({}, loaderOptions, { 40 | sourceMap: options.sourceMap 41 | }) 42 | }) 43 | } 44 | 45 | // Extract CSS when that option is specified 46 | // (which is the case during production build) 47 | if (options.extract) { 48 | return ExtractTextPlugin.extract({ 49 | use: loaders, 50 | fallback: 'vue-style-loader' 51 | }) 52 | } else { 53 | return ['vue-style-loader'].concat(loaders) 54 | } 55 | } 56 | 57 | // https://vue-loader.vuejs.org/en/configurations/extract-css.html 58 | return { 59 | css: generateLoaders(), 60 | postcss: generateLoaders(), 61 | less: generateLoaders('less'), 62 | sass: generateLoaders('sass', { indentedSyntax: true }), 63 | scss: generateLoaders('sass'), 64 | stylus: generateLoaders('stylus'), 65 | styl: generateLoaders('stylus') 66 | } 67 | } 68 | 69 | // Generate loaders for standalone style files (outside of .vue) 70 | exports.styleLoaders = function (options) { 71 | const output = [] 72 | const loaders = exports.cssLoaders(options) 73 | 74 | for (const extension in loaders) { 75 | const loader = loaders[extension] 76 | output.push({ 77 | test: new RegExp('\\.' + extension + '$'), 78 | use: loader 79 | }) 80 | } 81 | 82 | return output 83 | } 84 | 85 | exports.createNotifierCallback = () => { 86 | const notifier = require('node-notifier') 87 | 88 | return (severity, errors) => { 89 | if (severity !== 'error') return 90 | 91 | const error = errors[0] 92 | const filename = error.file && error.file.split('!').pop() 93 | 94 | notifier.notify({ 95 | title: packageConfig.name, 96 | message: severity + ': ' + error.name, 97 | subtitle: filename || '', 98 | icon: path.join(__dirname, 'logo.png') 99 | }) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /app-users/build/vue-loader.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const utils = require('./utils') 3 | const config = require('../config') 4 | const isProduction = process.env.NODE_ENV === 'production' 5 | const sourceMapEnabled = isProduction 6 | ? config.build.productionSourceMap 7 | : config.dev.cssSourceMap 8 | 9 | module.exports = { 10 | loaders: utils.cssLoaders({ 11 | sourceMap: sourceMapEnabled, 12 | extract: isProduction 13 | }), 14 | cssSourceMap: sourceMapEnabled, 15 | cacheBusting: config.dev.cacheBusting, 16 | transformToRequire: { 17 | video: ['src', 'poster'], 18 | source: 'src', 19 | img: 'src', 20 | image: 'xlink:href' 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app-users/build/webpack.base.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const utils = require('./utils') 4 | const config = require('../config') 5 | const vueLoaderConfig = require('./vue-loader.conf') 6 | 7 | function resolve (dir) { 8 | return path.join(__dirname, '..', dir) 9 | } 10 | 11 | 12 | 13 | module.exports = { 14 | context: path.resolve(__dirname, '../'), 15 | entry: { 16 | app: './src/main.js' 17 | }, 18 | output: { 19 | path: config.build.assetsRoot, 20 | filename: '[name].js', 21 | publicPath: process.env.NODE_ENV === 'production' 22 | ? config.build.assetsPublicPath 23 | : config.dev.assetsPublicPath 24 | }, 25 | resolve: { 26 | extensions: ['.js', '.vue', '.json'], 27 | alias: { 28 | 'vue$': 'vue/dist/vue.esm.js', 29 | '@': resolve('src'), 30 | } 31 | }, 32 | module: { 33 | rules: [ 34 | { 35 | test: /\.vue$/, 36 | loader: 'vue-loader', 37 | options: vueLoaderConfig 38 | }, 39 | { 40 | test: /\.js$/, 41 | loader: 'babel-loader', 42 | include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')] 43 | }, 44 | { 45 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 46 | loader: 'url-loader', 47 | options: { 48 | limit: 10000, 49 | name: utils.assetsPath('img/[name].[hash:7].[ext]') 50 | } 51 | }, 52 | { 53 | test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, 54 | loader: 'url-loader', 55 | options: { 56 | limit: 10000, 57 | name: utils.assetsPath('media/[name].[hash:7].[ext]') 58 | } 59 | }, 60 | { 61 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 62 | loader: 'url-loader', 63 | options: { 64 | limit: 10000, 65 | name: utils.assetsPath('fonts/[name].[hash:7].[ext]') 66 | } 67 | } 68 | ] 69 | }, 70 | node: { 71 | // prevent webpack from injecting useless setImmediate polyfill because Vue 72 | // source contains it (although only uses it if it's native). 73 | setImmediate: false, 74 | // prevent webpack from injecting mocks to Node native modules 75 | // that does not make sense for the client 76 | dgram: 'empty', 77 | fs: 'empty', 78 | net: 'empty', 79 | tls: 'empty', 80 | child_process: 'empty' 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /app-users/build/webpack.dev.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const utils = require('./utils') 3 | const webpack = require('webpack') 4 | const config = require('../config') 5 | const merge = require('webpack-merge') 6 | const path = require('path') 7 | const baseWebpackConfig = require('./webpack.base.conf') 8 | const CopyWebpackPlugin = require('copy-webpack-plugin') 9 | const HtmlWebpackPlugin = require('html-webpack-plugin') 10 | const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') 11 | const portfinder = require('portfinder') 12 | 13 | const HOST = process.env.HOST 14 | const PORT = process.env.PORT && Number(process.env.PORT) 15 | 16 | const devWebpackConfig = merge(baseWebpackConfig, { 17 | module: { 18 | rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true }) 19 | }, 20 | // cheap-module-eval-source-map is faster for development 21 | devtool: config.dev.devtool, 22 | 23 | // these devServer options should be customized in /config/index.js 24 | devServer: { 25 | clientLogLevel: 'warning', 26 | historyApiFallback: { 27 | rewrites: [ 28 | { from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') }, 29 | ], 30 | }, 31 | hot: true, 32 | contentBase: false, // since we use CopyWebpackPlugin. 33 | compress: true, 34 | host: HOST || config.dev.host, 35 | port: PORT || config.dev.port, 36 | open: config.dev.autoOpenBrowser, 37 | overlay: config.dev.errorOverlay 38 | ? { warnings: false, errors: true } 39 | : false, 40 | publicPath: config.dev.assetsPublicPath, 41 | proxy: config.dev.proxyTable, 42 | quiet: true, // necessary for FriendlyErrorsPlugin 43 | watchOptions: { 44 | poll: config.dev.poll, 45 | } 46 | }, 47 | plugins: [ 48 | new webpack.DefinePlugin({ 49 | 'process.env': require('../config/dev.env') 50 | }), 51 | new webpack.HotModuleReplacementPlugin(), 52 | new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update. 53 | new webpack.NoEmitOnErrorsPlugin(), 54 | // https://github.com/ampedandwired/html-webpack-plugin 55 | new HtmlWebpackPlugin({ 56 | filename: 'index.html', 57 | template: 'index.html', 58 | inject: true 59 | }), 60 | // copy custom static assets 61 | new CopyWebpackPlugin([ 62 | { 63 | from: path.resolve(__dirname, '../static'), 64 | to: config.dev.assetsSubDirectory, 65 | ignore: ['.*'] 66 | } 67 | ]) 68 | ] 69 | }) 70 | 71 | module.exports = new Promise((resolve, reject) => { 72 | portfinder.basePort = process.env.PORT || config.dev.port 73 | portfinder.getPort((err, port) => { 74 | if (err) { 75 | reject(err) 76 | } else { 77 | // publish the new Port, necessary for e2e tests 78 | process.env.PORT = port 79 | // add port to devServer config 80 | devWebpackConfig.devServer.port = port 81 | 82 | // Add FriendlyErrorsPlugin 83 | devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({ 84 | compilationSuccessInfo: { 85 | messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`], 86 | }, 87 | onErrors: config.dev.notifyOnErrors 88 | ? utils.createNotifierCallback() 89 | : undefined 90 | })) 91 | 92 | resolve(devWebpackConfig) 93 | } 94 | }) 95 | }) 96 | -------------------------------------------------------------------------------- /app-users/build/webpack.prod.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const utils = require('./utils') 4 | const webpack = require('webpack') 5 | const config = require('../config') 6 | const merge = require('webpack-merge') 7 | const baseWebpackConfig = require('./webpack.base.conf') 8 | const CopyWebpackPlugin = require('copy-webpack-plugin') 9 | const HtmlWebpackPlugin = require('html-webpack-plugin') 10 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 11 | const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin') 12 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin') 13 | 14 | const env = require('../config/prod.env') 15 | 16 | const webpackConfig = merge(baseWebpackConfig, { 17 | module: { 18 | rules: utils.styleLoaders({ 19 | sourceMap: config.build.productionSourceMap, 20 | extract: true, 21 | usePostCSS: true 22 | }) 23 | }, 24 | devtool: config.build.productionSourceMap ? config.build.devtool : false, 25 | output: { 26 | path: config.build.assetsRoot, 27 | filename: utils.assetsPath('js/[name].[chunkhash].js'), 28 | chunkFilename: utils.assetsPath('js/[id].[chunkhash].js') 29 | }, 30 | plugins: [ 31 | // http://vuejs.github.io/vue-loader/en/workflow/production.html 32 | new webpack.DefinePlugin({ 33 | 'process.env': env 34 | }), 35 | new UglifyJsPlugin({ 36 | uglifyOptions: { 37 | compress: { 38 | warnings: false 39 | } 40 | }, 41 | sourceMap: config.build.productionSourceMap, 42 | parallel: true 43 | }), 44 | // extract css into its own file 45 | new ExtractTextPlugin({ 46 | filename: utils.assetsPath('css/[name].[contenthash].css'), 47 | // Setting the following option to `false` will not extract CSS from codesplit chunks. 48 | // Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack. 49 | // It's currently set to `true` because we are seeing that sourcemaps are included in the codesplit bundle as well when it's `false`, 50 | // increasing file size: https://github.com/vuejs-templates/webpack/issues/1110 51 | allChunks: true, 52 | }), 53 | // Compress extracted CSS. We are using this plugin so that possible 54 | // duplicated CSS from different components can be deduped. 55 | new OptimizeCSSPlugin({ 56 | cssProcessorOptions: config.build.productionSourceMap 57 | ? { safe: true, map: { inline: false } } 58 | : { safe: true } 59 | }), 60 | // generate dist index.html with correct asset hash for caching. 61 | // you can customize output by editing /index.html 62 | // see https://github.com/ampedandwired/html-webpack-plugin 63 | new HtmlWebpackPlugin({ 64 | filename: config.build.index, 65 | template: 'index.html', 66 | inject: true, 67 | minify: { 68 | removeComments: true, 69 | collapseWhitespace: true, 70 | removeAttributeQuotes: true 71 | // more options: 72 | // https://github.com/kangax/html-minifier#options-quick-reference 73 | }, 74 | // necessary to consistently work with multiple chunks via CommonsChunkPlugin 75 | chunksSortMode: 'dependency' 76 | }), 77 | // keep module.id stable when vendor modules does not change 78 | new webpack.HashedModuleIdsPlugin(), 79 | // enable scope hoisting 80 | new webpack.optimize.ModuleConcatenationPlugin(), 81 | // split vendor js into its own file 82 | new webpack.optimize.CommonsChunkPlugin({ 83 | name: 'vendor', 84 | minChunks (module) { 85 | // any required modules inside node_modules are extracted to vendor 86 | return ( 87 | module.resource && 88 | /\.js$/.test(module.resource) && 89 | module.resource.indexOf( 90 | path.join(__dirname, '../node_modules') 91 | ) === 0 92 | ) 93 | } 94 | }), 95 | // extract webpack runtime and module manifest to its own file in order to 96 | // prevent vendor hash from being updated whenever app bundle is updated 97 | new webpack.optimize.CommonsChunkPlugin({ 98 | name: 'manifest', 99 | minChunks: Infinity 100 | }), 101 | // This instance extracts shared chunks from code splitted chunks and bundles them 102 | // in a separate chunk, similar to the vendor chunk 103 | // see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk 104 | new webpack.optimize.CommonsChunkPlugin({ 105 | name: 'app', 106 | async: 'vendor-async', 107 | children: true, 108 | minChunks: 3 109 | }), 110 | 111 | // copy custom static assets 112 | new CopyWebpackPlugin([ 113 | { 114 | from: path.resolve(__dirname, '../static'), 115 | to: config.build.assetsSubDirectory, 116 | ignore: ['.*'] 117 | } 118 | ]) 119 | ] 120 | }) 121 | 122 | if (config.build.productionGzip) { 123 | const CompressionWebpackPlugin = require('compression-webpack-plugin') 124 | 125 | webpackConfig.plugins.push( 126 | new CompressionWebpackPlugin({ 127 | asset: '[path].gz[query]', 128 | algorithm: 'gzip', 129 | test: new RegExp( 130 | '\\.(' + 131 | config.build.productionGzipExtensions.join('|') + 132 | ')$' 133 | ), 134 | threshold: 10240, 135 | minRatio: 0.8 136 | }) 137 | ) 138 | } 139 | 140 | if (config.build.bundleAnalyzerReport) { 141 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin 142 | webpackConfig.plugins.push(new BundleAnalyzerPlugin()) 143 | } 144 | 145 | module.exports = webpackConfig 146 | -------------------------------------------------------------------------------- /app-users/config/dev.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const merge = require('webpack-merge') 3 | const prodEnv = require('./prod.env') 4 | 5 | module.exports = merge(prodEnv, { 6 | NODE_ENV: '"development"' 7 | }) 8 | -------------------------------------------------------------------------------- /app-users/config/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | // Template version: 1.3.1 3 | // see http://vuejs-templates.github.io/webpack for documentation. 4 | 5 | const path = require('path') 6 | 7 | module.exports = { 8 | dev: { 9 | 10 | // Paths 11 | assetsSubDirectory: 'static', 12 | assetsPublicPath: '/', 13 | proxyTable: {}, 14 | 15 | // Various Dev Server settings 16 | host: 'localhost', // can be overwritten by process.env.HOST 17 | port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined 18 | autoOpenBrowser: false, 19 | errorOverlay: true, 20 | notifyOnErrors: true, 21 | poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions- 22 | 23 | 24 | /** 25 | * Source Maps 26 | */ 27 | 28 | // https://webpack.js.org/configuration/devtool/#development 29 | devtool: 'cheap-module-eval-source-map', 30 | 31 | // If you have problems debugging vue-files in devtools, 32 | // set this to false - it *may* help 33 | // https://vue-loader.vuejs.org/en/options.html#cachebusting 34 | cacheBusting: true, 35 | 36 | cssSourceMap: true 37 | }, 38 | 39 | build: { 40 | // Template for index.html 41 | index: path.resolve(__dirname, '../dist/index.html'), 42 | 43 | // Paths 44 | assetsRoot: path.resolve(__dirname, '../dist'), 45 | assetsSubDirectory: 'static', 46 | assetsPublicPath: '/', 47 | 48 | /** 49 | * Source Maps 50 | */ 51 | 52 | productionSourceMap: true, 53 | // https://webpack.js.org/configuration/devtool/#production 54 | devtool: '#source-map', 55 | 56 | // Gzip off by default as many popular static hosts such as 57 | // Surge or Netlify already gzip all static assets for you. 58 | // Before setting to `true`, make sure to: 59 | // npm install --save-dev compression-webpack-plugin 60 | productionGzip: false, 61 | productionGzipExtensions: ['js', 'css'], 62 | 63 | // Run the build command with an extra argument to 64 | // View the bundle analyzer report after build finishes: 65 | // `npm run build --report` 66 | // Set to `true` or `false` to always turn it on or off 67 | bundleAnalyzerReport: process.env.npm_config_report 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app-users/config/prod.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | NODE_ENV: '"production"' 4 | } 5 | -------------------------------------------------------------------------------- /app-users/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | app-idcards 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /app-users/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app-idcards", 3 | "version": "1.0.0", 4 | "description": "Ethereum and VueJS dApp for managing identity cards", 5 | "author": "DanieleFavi.com", 6 | "private": true, 7 | "scripts": { 8 | "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js", 9 | "start": "npm run dev", 10 | "build": "node build/build.js" 11 | }, 12 | "dependencies": { 13 | "vue": "^2.5.2", 14 | "vue-router": "^3.0.1", 15 | "web3": "^0.20.6" 16 | }, 17 | "devDependencies": { 18 | "autoprefixer": "^7.1.2", 19 | "babel-core": "^6.22.1", 20 | "babel-helper-vue-jsx-merge-props": "^2.0.3", 21 | "babel-loader": "^7.1.1", 22 | "babel-plugin-syntax-jsx": "^6.18.0", 23 | "babel-plugin-transform-runtime": "^6.22.0", 24 | "babel-plugin-transform-vue-jsx": "^3.5.0", 25 | "babel-preset-env": "^1.3.2", 26 | "babel-preset-stage-2": "^6.22.0", 27 | "chalk": "^2.0.1", 28 | "copy-webpack-plugin": "^4.0.1", 29 | "css-loader": "^0.28.0", 30 | "extract-text-webpack-plugin": "^3.0.0", 31 | "file-loader": "^1.1.4", 32 | "friendly-errors-webpack-plugin": "^1.6.1", 33 | "html-webpack-plugin": "^2.30.1", 34 | "node-notifier": "^5.1.2", 35 | "optimize-css-assets-webpack-plugin": "^3.2.0", 36 | "ora": "^1.2.0", 37 | "portfinder": "^1.0.13", 38 | "postcss-import": "^11.0.0", 39 | "postcss-loader": "^2.0.8", 40 | "postcss-url": "^7.2.1", 41 | "rimraf": "^2.6.0", 42 | "semver": "^5.3.0", 43 | "shelljs": "^0.7.6", 44 | "uglifyjs-webpack-plugin": "^1.1.1", 45 | "url-loader": "^0.5.8", 46 | "vue-loader": "^13.3.0", 47 | "vue-style-loader": "^3.0.1", 48 | "vue-template-compiler": "^2.5.2", 49 | "webpack": "^3.6.0", 50 | "webpack-bundle-analyzer": "^2.9.0", 51 | "webpack-dev-server": "^2.9.1", 52 | "webpack-merge": "^4.1.0" 53 | }, 54 | "engines": { 55 | "node": ">= 6.0.0", 56 | "npm": ">= 3.0.0" 57 | }, 58 | "browserslist": [ 59 | "> 1%", 60 | "last 2 versions", 61 | "not ie <= 8" 62 | ] 63 | } 64 | -------------------------------------------------------------------------------- /app-users/src/App.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 48 | 49 | 56 | -------------------------------------------------------------------------------- /app-users/src/assets/Users.json: -------------------------------------------------------------------------------- 1 | ONCE YOU DEPLOYED THE SMART CONTRACTS, PASTE HERE THE CONTENT OF THE FILE 2 | ~/ethereum-vuejs-dapp/build/contracts/Users.json 3 | -------------------------------------------------------------------------------- /app-users/src/components/TopMenu.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 104 | 105 | 107 | -------------------------------------------------------------------------------- /app-users/src/libs/BcExplorer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Daniele Favi 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * all copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | * SOFTWARE. 21 | */ 22 | 23 | // TODO: add function that checks if an address contract is valid 24 | 25 | import Web3 from 'web3' 26 | 27 | class BcExplorer { 28 | 29 | constructor() { 30 | this.web3inst = null; // store the web3 instace 31 | this.contractInst = []; // store the smart contract instance 32 | 33 | // general info about connection and user information 34 | this.info = { 35 | isConnected: false, 36 | networkId: 0, 37 | coinbase: null, 38 | mainAccount:null, 39 | balance: 0, 40 | addressUrl: null 41 | }; 42 | }; 43 | 44 | 45 | 46 | /** 47 | * Initialize the Web3 instance. 48 | * 49 | * @param {string} addressUrl Provider address URL like http://127.0.0.1:7545 50 | * @return {Promise} 51 | */ 52 | init(addressUrl) { 53 | return new Promise((resolve, reject) => { 54 | // checking if the provider is already set by mist or metamask 55 | if (window.ethereum) { 56 | window.web3 = new Web3(ethereum); 57 | 58 | try { 59 | // Request account access if needed 60 | ethereum.enable() 61 | .then(() => { 62 | this.setWeb3(window.web3, addressUrl); 63 | 64 | resolve(window.web3); 65 | }) 66 | .catch(error => reject(error)); 67 | } catch (error) { 68 | reject(error); 69 | } 70 | } else { 71 | if (typeof web3 !== 'undefined') { 72 | var web3js = new Web3(web3.currentProvider); 73 | } else { 74 | if (typeof addressUrl == 'undefined') { 75 | reject( new Error('BcExplorer error: impossible to connect.') ); 76 | } 77 | 78 | // set the provider you want from Web3.providers 79 | var provider = new Web3.providers.HttpProvider(addressUrl); 80 | 81 | try { 82 | var web3js = new Web3(provider); 83 | } catch (e) { 84 | // this is in case metamask/mist is off and avoid the exception web3 is not instatiated 85 | var web3js = new Web3(provider); 86 | } 87 | } 88 | 89 | this.setWeb3(web3js, addressUrl); 90 | resolve(web3js); 91 | } 92 | }) 93 | } 94 | 95 | 96 | 97 | /** 98 | * Set the Web3 instance and general information. 99 | * 100 | * @param {Object} web3js web3 instance 101 | * @param {string} addressUrl Provider address URL like http://127.0.0.1:7545 102 | * @return {void} 103 | */ 104 | setWeb3(web3js, addressUrl) { 105 | this.info.addressUrl = addressUrl; 106 | this.info.isConnected = web3js.isConnected(); // setting the connection 107 | this.web3inst = web3js; 108 | 109 | this.loadInfo(); 110 | } 111 | 112 | 113 | 114 | /** 115 | * Initialize a smart contract from the compiled JSON. 116 | * The compiledJson parameter is the JSON of the smart contract settings 117 | * that you can find in the folder /build/contracts/YourContract.json after 118 | * the migration. 119 | * 120 | * @param {Object} compiledJson compiled JSON from truffle 121 | * @param {string} contractName contract name (required if you are initializing more then one contract) 122 | * @return {Promise} 123 | */ 124 | initContractJson(compiledJson, contractName, networkId) { 125 | var networkId = networkId || null; 126 | 127 | // if the networkId is not provided it will find out 128 | if (! networkId) { 129 | return this.getNetworkId() 130 | .then(networkId => { 131 | return this.performInitContractJson(networkId, compiledJson, contractName); 132 | }) 133 | .catch(error => { 134 | return error; 135 | }) 136 | } 137 | 138 | return this.performInitContractJson(networkId, compiledJson, contractName); 139 | } 140 | 141 | 142 | 143 | /** 144 | * Initialize Web3 and a smart contract. 145 | * The compiledJson parameter is the JSON of the smart contract settings 146 | * that you can find in the folder /build/contracts/YourContract.json after 147 | * the migration. 148 | * 149 | * @param {Object} compiledJson 150 | * @param {string} addressUrl 151 | * @param {string} contractName contract name (required if you are initializing more then one contract) 152 | * @return {Promise} 153 | */ 154 | initWithContractJson(compiledJson, addressUrl, contractName, networkId) { 155 | return new Promise((resolve, reject) => { 156 | this.init(addressUrl) 157 | .then(() => resolve( this.initContractJson(compiledJson, contractName, networkId) )) 158 | .catch(error => reject(error)); 159 | }); 160 | } 161 | 162 | 163 | 164 | /** 165 | * Return the info set in the local class variable info. 166 | * 167 | * @param {string} attr 168 | * @return mixed 169 | */ 170 | getInfo(attr) { 171 | if (attr) return this.info[attr]; 172 | 173 | return this.info; 174 | } 175 | 176 | 177 | 178 | /** 179 | * Return the web3 instance. 180 | * If there is mist/metamask running on the client browser then it will 181 | * return the global web3 instance. Otherwise it return the local web3 instance. 182 | * 183 | * @return {object} 184 | */ 185 | web3() { 186 | if (typeof web3 !== 'undefined') return web3; 187 | else if (typeof window.web3 !== 'undefined') return window.web3; 188 | 189 | if (this.web3inst) return this.web3inst; 190 | 191 | console.error('BcExplorer error: Web3 is not initialized.'); 192 | } 193 | 194 | 195 | 196 | /** 197 | * Check if the connection with the blockchain is established and if the contract 198 | * is properly initialized. 199 | * 200 | * @return {bool} 201 | */ 202 | isConnected() { 203 | return this.info.isConnected && this.countContracts(); 204 | } 205 | 206 | 207 | 208 | /** 209 | * Return the contract instance. 210 | * 211 | * @return {object} 212 | */ 213 | contract(contractName) { 214 | if (this.countContracts() == 0) { 215 | console.error('BcExplorer error: contract is not initialized.'); 216 | 217 | return; 218 | } 219 | 220 | var contractName = this.contractDefaultName(contractName); 221 | 222 | if (typeof this.contractInst[contractName] == 'undefined') { 223 | console.error('BcExplorer error: contract does not exist.'); 224 | 225 | return; 226 | } 227 | 228 | return this.contractInst[contractName]; 229 | } 230 | 231 | 232 | 233 | /** 234 | * Initialize the smart contract. 235 | * 236 | * @param {number} networkId 237 | * @param {object} compiledJson Truffle compiled JSON after the migration of the smart contract (file you find in /build/contract after smart contract migration) 238 | * @return boolean 239 | */ 240 | performInitContractJson(networkId, compiledJson, contractName) { 241 | if (typeof compiledJson['abi'] == undefined) { 242 | console.error('BcExplorer error: missing ABI in the compiled Truffle JSON.'); 243 | return false; 244 | } 245 | 246 | var abiArray = compiledJson['abi']; 247 | 248 | if ( 249 | (typeof compiledJson['networks'] == undefined) || 250 | (compiledJson['networks'][networkId] == undefined) 251 | ) { 252 | console.error('BcExplorer error: missing networkId in the compiled Truffle JSON.'); 253 | 254 | return false; 255 | } 256 | 257 | var contractAddr = compiledJson['networks'][networkId].address; 258 | 259 | if (!this.web3().isAddress(contractAddr)) return false; 260 | 261 | this.initContract(abiArray, contractAddr, contractName); 262 | } 263 | 264 | 265 | 266 | /** 267 | * Initialize the smart contract. 268 | * 269 | * @param {object} abiArray ABI of the smart contract 270 | * @param {string} contractAddr Smart contract address 271 | * @param {string} contractName contract name (required if you are initializing more then one contract) 272 | * @return {void} 273 | */ 274 | initContract(abiArray, contractAddr, contractName) 275 | { 276 | var contractName = this.contractDefaultName(contractName); 277 | 278 | this.contractInst[contractName] = this.web3().eth.contract(abiArray).at(contractAddr); 279 | } 280 | 281 | 282 | 283 | /** 284 | * Return the newtwork ID of the connected blockchain. 285 | * 286 | * @return {Promise} 287 | */ 288 | getNetworkId() { 289 | return new Promise((resolve, reject) => { 290 | this.web3().version.getNetwork((error, networkId) => { 291 | if (error) { 292 | console.error(error); 293 | 294 | reject(new Error('BcExplorer error: networkId not available.')); 295 | } else { 296 | this.info.networkId = networkId; 297 | 298 | resolve(networkId); 299 | } 300 | }); 301 | }); 302 | } 303 | 304 | 305 | 306 | /** 307 | * Return the selected wallet address. 308 | * 309 | * @return {Promise} 310 | */ 311 | getMainAccount() { 312 | return new Promise((resolve, reject) => { 313 | if (this.info.mainAccount) resolve(this.info.mainAccount); 314 | 315 | this.web3().eth.getAccounts((error, accounts) => { 316 | if (error) { 317 | reject(new Error('BcExplorer error: accounts not available.')) 318 | } else { 319 | this.info.mainAccount = accounts[0]; 320 | 321 | resolve(accounts[0]); 322 | } 323 | }); 324 | }); 325 | } 326 | 327 | 328 | 329 | /** 330 | * Return the address of the current user. 331 | * 332 | * @return {Promise} 333 | */ 334 | getCoinbase() { 335 | return new Promise((resolve, reject) => { 336 | if (this.info.coinbase) resolve(this.info.coinbase); 337 | 338 | this.web3().eth.getCoinbase((error, coinbase) => { 339 | if (error) { 340 | reject(new Error('BcExplorer error: coinbase not available.')) 341 | } else { 342 | this.info.coinbase = coinbase; 343 | 344 | resolve(coinbase); 345 | } 346 | }); 347 | }); 348 | } 349 | 350 | 351 | 352 | /** 353 | * Return the balance in Wei of the user. 354 | * 355 | * @param {string} accountAddr 356 | * @return {Promise} 357 | */ 358 | getBalance(accountAddr) { 359 | return new Promise((resolve, reject) => { 360 | this.web3().eth.getBalance(accountAddr, (error, balance) => { 361 | if (error) { 362 | reject(new Error('BcExplorer error: impossible to get the balance for the account: ' + accountAddr)); 363 | } else { 364 | if (balance && (typeof balance == 'object')) { 365 | var bal = balance.toNumber(); 366 | this.info.balance = bal; 367 | 368 | resolve( bal ); 369 | } 370 | 371 | resolve(balance); 372 | } 373 | }); 374 | }); 375 | } 376 | 377 | 378 | 379 | /** 380 | * Load the generic info (coinbase, networkId and balance of the user). 381 | * 382 | * @return {Promise} 383 | */ 384 | async loadInfo() { 385 | try { 386 | var coinbase = await this.getCoinbase(); 387 | var mainAccount = await this.getMainAccount(); 388 | var balance = await this.getBalance(mainAccount); 389 | var networkId = await this.getNetworkId(); 390 | 391 | return Promise.resolve(this.info); 392 | } catch (e) { 393 | return Promise.reject(e); 394 | } 395 | 396 | // Nested promises: the following piece of code does the same the 397 | // previous block. 398 | // 399 | // return new Promise((resolve, reject) => { 400 | // this.getCoinbase().then(coinbase => { 401 | // this.getMainAccount().then(account => { 402 | // this.getBalance(account).then(balance => { 403 | // this.getNetworkId().then(networkId => { 404 | // resolve(this.info); 405 | // }).catch(error => reject(error)); 406 | // }).catch(error => reject(error)); 407 | // }).catch(error => reject(error)); 408 | // }).catch(error => reject(error)); 409 | // }); 410 | } 411 | 412 | 413 | 414 | /* ********************************************* */ 415 | /* ************* UTILITY FUNCTIONS ************* */ 416 | /* ********************************************* */ 417 | 418 | /** 419 | * Tranform the balance from Wei to Ether 420 | * 421 | * @param {mixed} bal 422 | * @return {numeric} 423 | */ 424 | weiToEther(bal) { 425 | if (typeof bal == 'object') { 426 | bal = bal.toNumber(); 427 | } 428 | 429 | return this.web3().fromWei(bal, "ether"); 430 | } 431 | 432 | 433 | 434 | /** 435 | * Transform the parameter from bytes to string. 436 | * 437 | * @param {string} bytes 438 | * @return {string} 439 | */ 440 | toAscii(bytes) { 441 | return this.web3().toAscii(bytes).replace(/\u0000/g, ''); 442 | } 443 | 444 | 445 | 446 | /** 447 | * Transform a timestamp number to date. 448 | * 449 | * @param {numeric} bytes 450 | * @return {string} 451 | */ 452 | toDate(timestamp) { 453 | return new Date(timestamp * 1000).toISOString(); 454 | } 455 | 456 | 457 | 458 | /** 459 | * Count the initialized contracts. Note that array of the initialized 460 | * contracts is an array key => value. 461 | * 462 | * @return {Number} 463 | */ 464 | countContracts() { 465 | var total = 0; 466 | 467 | for (var key in this.contractInst) { 468 | if (this.contractInst.hasOwnProperty(key)) total++; 469 | } 470 | 471 | return total; 472 | } 473 | 474 | 475 | 476 | /** 477 | * Return the string 'default' if the contract name is empty. 478 | * 479 | * @return {string} name 480 | * @return {string} 481 | */ 482 | contractDefaultName(name) { 483 | var contractName = name || 'default'; 484 | 485 | if (!contractName || !contractName.length) contractName = 'default'; 486 | 487 | return contractName; 488 | } 489 | 490 | 491 | } 492 | 493 | export default BcExplorer; 494 | -------------------------------------------------------------------------------- /app-users/src/libs/mixinViews.js: -------------------------------------------------------------------------------- 1 | import BcExplorer from './BcExplorer' 2 | import UsersContract from '../assets/Users.json'; 3 | 4 | export default { 5 | data() { 6 | return { 7 | bcConnected: false, // true when the connection with the blockchain is established, plus the contract ABI + address is correctli initialized 8 | bcConnectionError: false, 9 | errorConnectionMessage: null, 10 | bcSmartContractAddressError: false 11 | } 12 | }, 13 | 14 | created() { 15 | this.init(); 16 | }, 17 | 18 | methods: { 19 | /** 20 | * Initialize the BcExplore object (this means the connection with the 21 | * blockchin and initialise the contracts). 22 | * 23 | * @return {void} 24 | */ 25 | init() { 26 | // when this file is imported to other component it checks if the BcExplorer 27 | // is instatiated. 28 | if (window.bc == undefined) { 29 | window.bc = new BcExplorer; 30 | 31 | // connecting to the blockchain and intializing the Users smart contract 32 | window.bc.initWithContractJson(UsersContract, 'http://127.0.0.1:7545') 33 | .then((error) => { 34 | // handling the connection error 35 | if (error) { 36 | this.bcConnected = false; 37 | 38 | this.showConnectionErrorMessage(error); 39 | } else { 40 | // calling a smart contract function in order to check the contract address 41 | // is correct. NOTE: here you might be connected successfully. 42 | // TODO: the check of the smart contract address validity it should be BcExplorer duty 43 | this.isRegistered() 44 | .then(res => { 45 | this.bcConnectionError = false; 46 | this.bcConnected = this.blockchainIsConnected(); 47 | }) 48 | .catch(error => { 49 | this.showConnectionErrorMessage(error); 50 | this.bcSmartContractAddressError = true; 51 | }); 52 | } 53 | }) 54 | .catch(error => this.showConnectionErrorMessage(error)); 55 | } // end if (window.bc == undefined) 56 | }, 57 | 58 | /** 59 | * Check if the user is registered. 60 | * 61 | * @return {Promise} 62 | */ 63 | isRegistered() { 64 | return new Promise((resolve, reject) => { 65 | window.bc.getMainAccount() 66 | .then(account => { 67 | window.bc.contract().isRegistered({ from: account }, (error, res) => { 68 | if (error) reject(error); 69 | 70 | resolve(res); 71 | }); 72 | }) 73 | .catch(error => reject(error)); 74 | }); 75 | }, 76 | 77 | /** 78 | * Show the conntection error message on top of the main content. 79 | * 80 | * @param {object} error|null 81 | * @return {void} 82 | */ 83 | showConnectionErrorMessage(error=null) { 84 | this.bcConnectionError = true; 85 | 86 | if (error) console.log(error); 87 | 88 | if (error && error.message) { 89 | this.errorConnectionMessage = error.message; 90 | } 91 | }, 92 | 93 | /** 94 | * Check if the connection with the blockchain is established and if the smart 95 | * contract ABI + address are correctly initialized. 96 | * 97 | * @return {boolean} 98 | */ 99 | blockchainIsConnected() { 100 | this.bcConnected = ((window.bc != undefined) && window.bc.isConnected()); 101 | 102 | return this.bcConnected; 103 | }, 104 | 105 | /** 106 | * Transform the parameter from bytes to string. 107 | * 108 | * @param {string} bytesStr 109 | * @return {string} 110 | */ 111 | toAscii(bytesStr) { 112 | return window.bc.toAscii(bytesStr); 113 | }, 114 | 115 | /** 116 | * Transform a timestamp number to date. 117 | * 118 | * @param {numeric} bytesStr 119 | * @return {string} 120 | */ 121 | toDate(timestamp) { 122 | return new Date(timestamp * 1000).toISOString(); 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /app-users/src/main.js: -------------------------------------------------------------------------------- 1 | // The Vue build version to load with the `import` command 2 | // (runtime-only or standalone) has been set in webpack.base.conf with an alias. 3 | import Vue from 'vue'; 4 | import App from './App'; 5 | import router from './routes'; 6 | 7 | window.Event = new Vue(); // global event 8 | 9 | Vue.config.productionTip = false; 10 | 11 | /* eslint-disable no-new */ 12 | new Vue({ 13 | el: '#app', 14 | router, 15 | components: { App }, 16 | template: '', 17 | }); 18 | -------------------------------------------------------------------------------- /app-users/src/routes.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Router from 'vue-router'; 3 | import Profile from '@/views/Profile'; 4 | import List from '@/views/List'; 5 | import Register from '@/views/Register'; 6 | 7 | Vue.use(Router); 8 | 9 | export default new Router({ 10 | routes: [ 11 | { 12 | path: '/', 13 | name: 'List', 14 | component: List 15 | }, 16 | { 17 | path: '/profile', 18 | name: 'Profile', 19 | component: Profile 20 | }, 21 | { 22 | path: '/register', 23 | name: 'Register', 24 | component: Register 25 | }, 26 | ], 27 | linkActiveClass: 'active' 28 | }); 29 | -------------------------------------------------------------------------------- /app-users/src/views/List.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 119 | 120 | 122 | -------------------------------------------------------------------------------- /app-users/src/views/Profile.vue: -------------------------------------------------------------------------------- 1 | 51 | 52 | 248 | -------------------------------------------------------------------------------- /app-users/src/views/Register.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 201 | -------------------------------------------------------------------------------- /app-users/static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielefavi/ethereum-vuejs-dapp/e397cc0a0afc85671828926982f3788cbd60d62d/app-users/static/.gitkeep -------------------------------------------------------------------------------- /contracts/Migrations.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.0; 2 | 3 | 4 | 5 | contract Migrations { 6 | address public owner; 7 | uint public last_completed_migration; 8 | 9 | modifier restricted() { 10 | if (msg.sender == owner) _; 11 | } 12 | 13 | constructor() public { 14 | owner = msg.sender; 15 | } 16 | 17 | function setCompleted(uint completed) public restricted { 18 | last_completed_migration = completed; 19 | } 20 | 21 | function upgrade(address new_address) public restricted { 22 | Migrations upgraded = Migrations(new_address); 23 | upgraded.setCompleted(last_completed_migration); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /contracts/Users.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.0; 2 | 3 | 4 | 5 | contract Users { 6 | // data structure that stores a user 7 | struct User { 8 | string name; 9 | bytes32 status; 10 | address walletAddress; 11 | uint createdAt; 12 | uint updatedAt; 13 | } 14 | 15 | // it maps the user's wallet address with the user ID 16 | mapping (address => uint) public usersIds; 17 | 18 | // Array of User that holds the list of users and their details 19 | User[] public users; 20 | 21 | // event fired when an user is registered 22 | event newUserRegistered(uint id); 23 | 24 | // event fired when the user updates his status or name 25 | event userUpdateEvent(uint id); 26 | 27 | 28 | 29 | // Modifier: check if the caller of the smart contract is registered 30 | modifier checkSenderIsRegistered { 31 | require(isRegistered()); 32 | _; 33 | } 34 | 35 | 36 | 37 | /** 38 | * Constructor function 39 | */ 40 | constructor() public 41 | { 42 | // NOTE: the first user MUST be emtpy: if you are trying to access to an element 43 | // of the usersIds mapping that does not exist (like usersIds[0x12345]) you will 44 | // receive 0, that's why in the first position (with index 0) must be initialized 45 | addUser(address(0x0), "", ""); 46 | 47 | // Some dummy data 48 | addUser(address(0x333333333333), "Leo Brown", "Available"); 49 | addUser(address(0x111111111111), "John Doe", "Very happy"); 50 | addUser(address(0x222222222222), "Mary Smith", "Not in the mood today"); 51 | } 52 | 53 | 54 | 55 | /** 56 | * Function to register a new user. 57 | * 58 | * @param _userName The displaying name 59 | * @param _status The status of the user 60 | */ 61 | function registerUser(string memory _userName, bytes32 _status) public 62 | returns(uint) 63 | { 64 | return addUser(msg.sender, _userName, _status); 65 | } 66 | 67 | 68 | 69 | /** 70 | * Add a new user. This function must be private because an user 71 | * cannot insert another user on behalf of someone else. 72 | * 73 | * @param _wAddr Address wallet of the user 74 | * @param _userName Displaying name of the user 75 | * @param _status Status of the user 76 | */ 77 | function addUser(address _wAddr, string memory _userName, bytes32 _status) private 78 | returns(uint) 79 | { 80 | // checking if the user is already registered 81 | uint userId = usersIds[_wAddr]; 82 | require (userId == 0); 83 | 84 | // associating the user wallet address with the new ID 85 | usersIds[_wAddr] = users.length; 86 | uint newUserId = users.length++; 87 | 88 | // storing the new user details 89 | users[newUserId] = User({ 90 | name: _userName, 91 | status: _status, 92 | walletAddress: _wAddr, 93 | createdAt: now, 94 | updatedAt: now 95 | }); 96 | 97 | // emitting the event that a new user has been registered 98 | emit newUserRegistered(newUserId); 99 | 100 | return newUserId; 101 | } 102 | 103 | 104 | 105 | /** 106 | * Update the user profile of the caller of this method. 107 | * Note: the user can modify only his own profile. 108 | * 109 | * @param _newUserName The new user's displaying name 110 | * @param _newStatus The new user's status 111 | */ 112 | function updateUser(string memory _newUserName, bytes32 _newStatus) checkSenderIsRegistered public 113 | returns(uint) 114 | { 115 | // An user can modify only his own profile. 116 | uint userId = usersIds[msg.sender]; 117 | 118 | User storage user = users[userId]; 119 | 120 | user.name = _newUserName; 121 | user.status = _newStatus; 122 | user.updatedAt = now; 123 | 124 | emit userUpdateEvent(userId); 125 | 126 | return userId; 127 | } 128 | 129 | 130 | 131 | /** 132 | * Get the user's profile information. 133 | * 134 | * @param _id The ID of the user stored on the blockchain. 135 | */ 136 | function getUserById(uint _id) public view 137 | returns( 138 | uint, 139 | string memory, 140 | bytes32, 141 | address, 142 | uint, 143 | uint 144 | ) { 145 | // checking if the ID is valid 146 | require( (_id > 0) || (_id <= users.length) ); 147 | 148 | User memory i = users[_id]; 149 | 150 | return ( 151 | _id, 152 | i.name, 153 | i.status, 154 | i.walletAddress, 155 | i.createdAt, 156 | i.updatedAt 157 | ); 158 | } 159 | 160 | 161 | 162 | /** 163 | * Return the profile information of the caller. 164 | */ 165 | function getOwnProfile() checkSenderIsRegistered public view 166 | returns( 167 | uint, 168 | string memory, 169 | bytes32, 170 | address, 171 | uint, 172 | uint 173 | ) { 174 | uint id = usersIds[msg.sender]; 175 | 176 | return getUserById(id); 177 | } 178 | 179 | 180 | 181 | /** 182 | * Check if the user that is calling the smart contract is registered. 183 | */ 184 | function isRegistered() public view returns (bool) 185 | { 186 | return (usersIds[msg.sender] > 0); 187 | } 188 | 189 | 190 | 191 | /** 192 | * Return the number of total registered users. 193 | */ 194 | function totalUsers() public view returns (uint) 195 | { 196 | // NOTE: the total registered user is length-1 because the user with 197 | // index 0 is empty check the contructor: addUser(address(0x0), "", ""); 198 | return users.length - 1; 199 | } 200 | 201 | } 202 | -------------------------------------------------------------------------------- /migrations/1_initial_migration.js: -------------------------------------------------------------------------------- 1 | var Migrations = artifacts.require("./Migrations.sol"); 2 | 3 | module.exports = function(deployer) { 4 | deployer.deploy(Migrations); 5 | }; 6 | -------------------------------------------------------------------------------- /migrations/2_migrate_users.js: -------------------------------------------------------------------------------- 1 | var Users = artifacts.require("./Users.sol"); 2 | 3 | module.exports = function(deployer) { 4 | deployer.deploy(Users); 5 | }; 6 | -------------------------------------------------------------------------------- /test/users.js: -------------------------------------------------------------------------------- 1 | var Users = artifacts.require('./Users.sol'); 2 | 3 | contract('Users', function(accounts) { 4 | var instance = null; // store the Users contract instance 5 | var mainAccount = accounts[0]; 6 | var anotherAccount = accounts[1]; 7 | 8 | // TEST: register a new user and check if the total users is increased and if the 9 | // user has been registered correctly. 10 | it("should register an user", function() { 11 | var usersBeforeRegister = null; 12 | 13 | return Users.deployed().then(function(contractInstance) { 14 | // storing the contract instance so it will be used later on 15 | instance = contractInstance; 16 | 17 | // calling the smart contract function totalUsers to get the current number of users 18 | return instance.totalUsers.call(); 19 | }).then(function(result) { 20 | // storing the current number on the var usersBeforeRegister 21 | usersBeforeRegister = result.toNumber(); 22 | 23 | // registering the user calling the smart contract function registerUser 24 | return instance.registerUser('Test User Name', 'Test Status', { 25 | from: mainAccount 26 | }); 27 | }).then(function(result) { 28 | return instance.totalUsers.call(); 29 | }).then(function(result) { 30 | // checking if the total number of user is increased by 1 31 | assert.equal(result.toNumber(), (usersBeforeRegister+1), "number of users must be (" + usersBeforeRegister + " + 1)"); 32 | 33 | // calling the smart contract function isRegistered to know if the sender is registered. 34 | return instance.isRegistered.call(); 35 | }).then(function(result) { 36 | // we are expecting a boolean in return that it should be TRUE 37 | assert.isTrue(result); 38 | }); 39 | }); // end of "should register an user" 40 | 41 | 42 | 43 | // Testing the data of the user profile stored in the blockchain match with the data 44 | // gave during the registration. 45 | it("username and status in the blockchian should be the same the one gave on the registration", function() { 46 | // NOTE: the contract instance has been instantiated before, so no need 47 | // to do again: return Users.deployed().then(function(contractInstance) { ... 48 | // like before in "should register an user". 49 | return instance.getOwnProfile.call().then(function(result) { 50 | // the result is an array where in the position 0 there user ID, in 51 | // the position 1 the user name and in the position 2 the status, 52 | assert.equal(result[1], 'Test User Name'); 53 | 54 | // the status is type of bytes32: converting the status Bytes32 into string 55 | let newStatusStr = web3.toAscii(result[2]).replace(/\u0000/g, ''); 56 | assert.equal(newStatusStr, 'Test Status'); 57 | }); 58 | }); // end testing username and status 59 | 60 | 61 | 62 | // Testing the update profile function: first update the user's profile name and status, then 63 | // chching that the profile has been updated correctly. 64 | it("should update the profile", function() { 65 | return instance.updateUser('Updated Name', 'Updated Status', { 66 | from: mainAccount 67 | }).then(function(result) { 68 | return instance.getOwnProfile.call(); 69 | }).then(function(result) { 70 | // the result is an array where in the position 0 there user ID, in 71 | // the position 1 the user name and in the position 2 the status, 72 | assert.equal(result[1], 'Updated Name'); 73 | 74 | // the status is type of bytes32: converting the status Bytes32 into string 75 | let newStatusStr = web3.toAscii(result[2]).replace(/\u0000/g, ''); 76 | assert.equal(newStatusStr, 'Updated Status'); 77 | }); 78 | }); // end should update the profile 79 | 80 | 81 | 82 | // Testing that a user cannot register itself twice. 83 | it("a registered user should not be registered twice", function() { 84 | // we are expecting the call to registerUser to fail since the user account 85 | // is already registered! 86 | return instance.registerUser('Test username Twice', 'Test Status Twice', { 87 | from: mainAccount 88 | }).then(assert.fail).catch(function(error) { // here we are expecting the exception 89 | assert(true); 90 | }); 91 | }); // end testing registration twice 92 | 93 | }); // end Users contract 94 | -------------------------------------------------------------------------------- /truffle.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | networks: { 3 | ganache: { 4 | host: "127.0.0.1", 5 | port: 7545, 6 | network_id: "*" 7 | }, 8 | privatebc: { 9 | host: "127.0.0.1", 10 | port: 8545, 11 | network_id: "4224" 12 | }, 13 | } 14 | }; 15 | --------------------------------------------------------------------------------