├── contract ├── .eslintignore ├── index.js ├── .editorconfig ├── .npmignore ├── .eslintrc.js ├── transaction_data │ └── freedom-dividend-transactions.txdata ├── package.json ├── contract-metadata │ └── metadata-sample.json ├── lib │ └── freedomDividendContract.js └── test │ └── freedom-dividend-contract.js ├── webapp ├── client │ ├── babel.config.js │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── logo.svg │ │ └── chainstackLogo.svg │ ├── src │ │ ├── assets │ │ │ ├── logo.png │ │ │ └── logo.svg │ │ ├── plugins │ │ │ └── vuetify.js │ │ ├── router │ │ │ └── index.js │ │ ├── main.js │ │ ├── App.vue │ │ ├── components │ │ │ ├── ActionButton.vue │ │ │ └── ChaincodeTransactions.vue │ │ └── views │ │ │ ├── Chaincode.vue │ │ │ └── Main.vue │ ├── .editorconfig │ ├── README.md │ ├── vue.config.js │ └── package.json ├── server │ ├── .babelrc │ ├── .edtiroconfig │ ├── .env │ ├── cli │ │ ├── index.js │ │ ├── peer.js │ │ └── scripts │ │ │ └── chaincode.sh │ ├── fabric │ │ ├── gateway.js │ │ ├── wallet.js │ │ └── utils │ │ │ └── helper.js │ ├── README.md │ ├── package.json │ ├── app.js │ └── api │ │ └── index.js └── certs │ └── README.md ├── .gitignore ├── downloadPeerBinary.sh ├── LICENSE └── README.md /contract/.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | -------------------------------------------------------------------------------- /webapp/client/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset', 4 | ], 5 | }; 6 | -------------------------------------------------------------------------------- /webapp/client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chainstacklabs/freedom-dividend-chaincode/HEAD/webapp/client/public/favicon.ico -------------------------------------------------------------------------------- /webapp/client/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chainstacklabs/freedom-dividend-chaincode/HEAD/webapp/client/src/assets/logo.png -------------------------------------------------------------------------------- /webapp/server/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env" 4 | ], 5 | "plugins": [ 6 | ["module-resolver", { 7 | "root": ["./"] 8 | }] 9 | ] 10 | } -------------------------------------------------------------------------------- /webapp/server/.edtiroconfig: -------------------------------------------------------------------------------- 1 | [*.*] 2 | indent_style = space 3 | indent_size = 2 4 | end_of_line = lf 5 | trim_trailing_whitespace = true 6 | insert_final_newline = true 7 | max_line_length = 100 8 | -------------------------------------------------------------------------------- /webapp/client/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx,vue}] 2 | root = true 3 | indent_style = space 4 | indent_size = 2 5 | end_of_line = lf 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | max_line_length = 100 9 | -------------------------------------------------------------------------------- /contract/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const FreedomDividendContract = require('./lib/freedomDividendContract.js'); 4 | 5 | module.exports.FreedomDividendContract = FreedomDividendContract; 6 | module.exports.contracts = [ FreedomDividendContract ]; 7 | -------------------------------------------------------------------------------- /contract/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /contract/.npmignore: -------------------------------------------------------------------------------- 1 | # don't package the connection details 2 | local_fabric 3 | 4 | # don't package the tests 5 | test 6 | functionalTests 7 | 8 | # don't package config files 9 | .vscode 10 | .editorconfig 11 | .eslintignore 12 | .eslintrc.js 13 | .gitignore 14 | .npmignore 15 | .nyc_output 16 | coverage 17 | -------------------------------------------------------------------------------- /webapp/server/.env: -------------------------------------------------------------------------------- 1 | AS_LOCALHOST=false 2 | 3 | CHANNEL_ID=defaultchannel 4 | CHAINCODE_NAME=freedomDividendContract 5 | CHAINCODE_VERSION=1 6 | CHAINCODE_SEQUENCE=1 7 | 8 | # Org 9 | MSP_ID=Org1MSP 10 | 11 | # Orderer 12 | ORDERER_NAME=orderer.example.com 13 | 14 | # Peer 15 | PEER_NAME=peer0.org1.example.com -------------------------------------------------------------------------------- /webapp/client/README.md: -------------------------------------------------------------------------------- 1 | # Frontend 2 | 3 | ## Project setup 4 | 5 | ```bash 6 | npm install 7 | ``` 8 | 9 | ### Compiles and hot-reloads for development 10 | 11 | ```bash 12 | npm run serve 13 | ``` 14 | 15 | ### Compiles and minifies for production 16 | 17 | ```bash 18 | npm run build 19 | ``` 20 | 21 | ### Lints and fixes files 22 | 23 | ```bash 24 | npm run lint 25 | ``` 26 | 27 | ### Customize configuration 28 | 29 | See [Configuration Reference](https://cli.vuejs.org/config/). 30 | -------------------------------------------------------------------------------- /webapp/client/src/plugins/vuetify.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuetify from 'vuetify'; 3 | import 'vuetify/dist/vuetify.min.css'; 4 | 5 | Vue.use(Vuetify); 6 | 7 | export default new Vuetify({ 8 | theme: { 9 | themes: { 10 | light: { 11 | primary: '#007bff', 12 | secondary: '#424242', 13 | accent: '#82B1FF', 14 | error: '#FF5252', 15 | info: '#2196F3', 16 | success: '#4CAF50', 17 | warning: '#FFC107', 18 | }, 19 | }, 20 | }, 21 | }); 22 | -------------------------------------------------------------------------------- /webapp/client/src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | Artboard 46 2 | -------------------------------------------------------------------------------- /webapp/client/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import VueRouter from 'vue-router'; 3 | 4 | Vue.use(VueRouter); 5 | 6 | const routes = [ 7 | { 8 | path: '/', 9 | name: 'Main', 10 | component: () => import(/* webpackChunkName: "about" */ '../views/Main.vue'), 11 | }, 12 | { 13 | path: '/chaincode/:chaincode', 14 | name: 'Chaincode', 15 | component: () => import(/* webpackChunkName: "about" */ '../views/Chaincode.vue'), 16 | }, 17 | ]; 18 | 19 | const router = new VueRouter({ 20 | mode: 'history', 21 | base: process.env.BASE_URL, 22 | routes, 23 | }); 24 | 25 | export default router; 26 | -------------------------------------------------------------------------------- /webapp/client/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import axios from 'axios'; 3 | import TreeView from 'vue-json-tree-view'; 4 | import App from './App.vue'; 5 | import router from './router'; 6 | import vuetify from './plugins/vuetify'; 7 | import 'roboto-fontface/css/roboto/roboto-fontface.css'; 8 | import '@mdi/font/css/materialdesignicons.css'; 9 | 10 | axios.defaults.baseURL = '/api/v1'; 11 | axios.defaults.withCredentials = true; 12 | 13 | window.$eventHub = new Vue(); 14 | Vue.prototype.$http = axios; 15 | 16 | Vue.config.productionTip = false; 17 | Vue.use(TreeView); 18 | 19 | new Vue({ 20 | router, 21 | vuetify, 22 | render: (h) => h(App), 23 | }).$mount('#app'); 24 | -------------------------------------------------------------------------------- /webapp/client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /webapp/server/cli/index.js: -------------------------------------------------------------------------------- 1 | const util = require('util'); 2 | const exec = util.promisify(require('child_process').exec); 3 | const execFile = util.promisify(require('child_process').execFile); 4 | const { rootPath, generateCertPath } = require('../fabric/utils/helper'); 5 | 6 | const unlockScriptFolder = () => exec(`chmod -R 777 ${rootPath}/webapp/server/cli/scripts`); 7 | const execute = async (ARGS) => { 8 | const certs = await generateCertPath(); 9 | 10 | return execFile(`${rootPath}/webapp/server/cli/scripts/chaincode.sh`, [], { 11 | env: Object.assign( 12 | certs, 13 | ARGS, 14 | ), 15 | maxBuffer: 10 * 1024 * 1024 16 | }); 17 | }; 18 | 19 | module.exports = { 20 | execute, 21 | unlockScriptFolder, 22 | }; 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | *.log 3 | *.njsproj 4 | *.ntvs* 5 | *.pid 6 | *.pid.lock 7 | *.seed 8 | *.sln 9 | *.suo 10 | *.sw? 11 | *.sw? 12 | *.tgz 13 | *.tar.gz 14 | 15 | .cache 16 | .DS_Store 17 | .env.*.local 18 | .env.local 19 | .eslintcache 20 | .grunt 21 | .idea 22 | .lock-wscript 23 | .next 24 | .node_repl_history 25 | .npm 26 | .nuxt 27 | .nyc_output 28 | .serverless 29 | .txt 30 | .vscode 31 | .yarn-integrity 32 | 33 | bower_components 34 | coverage 35 | dist 36 | jspm_packages/ 37 | lib-cov 38 | logs 39 | node_modules/ 40 | npm-debug.log* 41 | pids 42 | typings/ 43 | yarn-error.log* 44 | 45 | # fabric 46 | wallets 47 | hlf 48 | chaincodes/*.tar.gz 49 | connection-profile.json 50 | connection-profile?.json 51 | /webapp/certs/* 52 | !/webapp/certs/README.md -------------------------------------------------------------------------------- /webapp/client/vue.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | outputDir: path.resolve(__dirname, '../dist'), 5 | devServer: { 6 | proxy: { 7 | '/api/v1': { 8 | target: 'http://localhost:4000', 9 | }, 10 | }, 11 | }, 12 | configureWebpack: { 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.s[ac]ss$/i, 17 | use: [ 18 | 'sass-loader', 19 | { 20 | loader: 'sass-loader', 21 | options: { 22 | /* eslint-disable global-require */ 23 | implementation: require('sass'), 24 | /* eslint-enable global-require */ 25 | sassOptions: { 26 | fiber: false, 27 | }, 28 | }, 29 | }, 30 | ], 31 | }, 32 | ], 33 | }, 34 | }, 35 | }; 36 | -------------------------------------------------------------------------------- /webapp/server/fabric/gateway.js: -------------------------------------------------------------------------------- 1 | import { Gateway } from 'fabric-network'; 2 | import { rootPath, seralizePath } from 'fabric/utils/helper'; 3 | import { connect as connectWallet } from 'fabric/wallet'; 4 | const envfile = require('envfile'); 5 | 6 | const gateway = new Gateway(); 7 | 8 | const connect = async identity => { 9 | const parsedFile = envfile.parseFileSync(`${rootPath}/webapp/server/.env`); 10 | const connectionProfile = JSON.parse(seralizePath(`${rootPath}/webapp/certs/connection-profile.json`)); 11 | const wallet = await connectWallet(); 12 | 13 | console.log(`==========AS_LOCALHOST: ${parsedFile.AS_LOCALHOST}==========`); 14 | await gateway.connect(connectionProfile, { 15 | identity: 'user01', 16 | wallet, 17 | discovery: { enabled: true, asLocalhost: (parsedFile.AS_LOCALHOST === 'true') } 18 | }); 19 | }; 20 | 21 | module.exports = { 22 | gateway, 23 | connect, 24 | }; 25 | -------------------------------------------------------------------------------- /webapp/certs/README.md: -------------------------------------------------------------------------------- 1 | ### Export the required files 2 | 3 | In [Chainstack](https://console.chainstack.com/): 4 | 5 | 1. Network connection profile: 6 | - Navigate to your Hyperledger Fabric network. 7 | - Click **Details**. 8 | - Click **Export connection profile**. 9 | - Move the exported file to the `webapp/certs/` directory. 10 | 1. Orderer TLS certificate: 11 | - Navigate to the Hyperledger Fabric **Service nodes** tab from the network. 12 | - Access **Orderer**. 13 | - Click **Export** 14 | - Unzip the downloaded folder 15 | - Move `-cert.pem` file to the `webapp/certs/` directory. 16 | 1. Organization identity zip folder: 17 | - Navigate to your Hyperledger Fabric network. 18 | - Click **Details**. 19 | - Access Admin identity 20 | - Click **Export** 21 | - Unzip the downloaded folder 22 | - Move `msp` subdirectory to the `webapp/certs/` directory. 23 | -------------------------------------------------------------------------------- /downloadPeerBinary.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | export FABRIC_VERSION=2.2.0 3 | export FABRIC_PATH="$(dirname "$0")/hlf" 4 | export FABRIC_BIN_PATH=${FABRIC_PATH}/bin 5 | export FABRIC_CFG_PATH=${FABRIC_PATH}/config 6 | export FABRIC_SOURCES_PATH=${FABRIC_PATH}/sources 7 | 8 | if [[ $1 == "linux" ]] 9 | then 10 | export FABRIC_BINARY_FILE="hyperledger-fabric-linux-amd64-${FABRIC_VERSION}.tar.gz" 11 | else 12 | export FABRIC_BINARY_FILE="hyperledger-fabric-darwin-amd64-${FABRIC_VERSION}.tar.gz" 13 | fi 14 | 15 | mkdir -p ${FABRIC_SOURCES_PATH} # Also creates FABRIC_PATH 16 | curl -sSL https://github.com/hyperledger/fabric/releases/download/v${FABRIC_VERSION}/${FABRIC_BINARY_FILE} \ 17 | | tar xz -C ${FABRIC_PATH} 18 | curl -sSL https://github.com/hyperledger/fabric/archive/v${FABRIC_VERSION}.tar.gz \ 19 | | tar xz -C ${FABRIC_SOURCES_PATH} 20 | mv ${FABRIC_SOURCES_PATH}/fabric-${FABRIC_VERSION}/sampleconfig/* ${FABRIC_CFG_PATH} 21 | rm -rf ${FABRIC_SOURCES_PATH} 22 | ls ${FABRIC_BIN_PATH} 23 | -------------------------------------------------------------------------------- /contract/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | node: true, 4 | mocha: true 5 | }, 6 | parserOptions: { 7 | ecmaVersion: 8, 8 | sourceType: 'script' 9 | }, 10 | extends: "eslint:recommended", 11 | rules: { 12 | indent: ['error', 4], 13 | quotes: ['error', 'single'], 14 | semi: ['error', 'always'], 15 | 'no-unused-vars': ['error', { args: 'none' }], 16 | 'no-console': 'off', 17 | curly: 'error', 18 | eqeqeq: 'error', 19 | 'no-throw-literal': 'error', 20 | strict: 'error', 21 | 'no-var': 'error', 22 | 'dot-notation': 'error', 23 | 'no-tabs': 'error', 24 | 'no-trailing-spaces': 'error', 25 | 'no-use-before-define': 'error', 26 | 'no-useless-call': 'error', 27 | 'no-with': 'error', 28 | 'operator-linebreak': 'error', 29 | yoda: 'error', 30 | 'quote-props': ['error', 'as-needed'] 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Chainstack 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 | -------------------------------------------------------------------------------- /webapp/server/fabric/wallet.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { Wallets } = require('fabric-network'); 3 | const { generateCertPath, seralizePath } = require('./utils/helper'); 4 | 5 | const connect = () => Wallets.newFileSystemWallet(path.join(process.cwd(), '/fabric/wallets')); 6 | const register = async identityLabel => { 7 | try { 8 | const { ADMIN_CERT, ADMIN_PRIVATE_KEY, MSP_ID: mspId } = await generateCertPath(); 9 | const certificate = seralizePath(ADMIN_CERT); 10 | const privateKey = seralizePath(ADMIN_PRIVATE_KEY); 11 | 12 | const wallet = await connect(); 13 | 14 | const existingIdentity = await wallet.get(identityLabel); 15 | if (existingIdentity) { 16 | await wallet.remove(identityLabel); 17 | } 18 | 19 | await wallet.put(identityLabel, { 20 | credentials: { 21 | certificate, 22 | privateKey, 23 | }, 24 | mspId, 25 | type: 'X.509', 26 | }); 27 | } catch (error) { 28 | console.log(`Error adding to wallet. ${error}`); 29 | console.log(error.stack); 30 | } 31 | }; 32 | 33 | module.exports = { 34 | register, 35 | connect, 36 | }; 37 | -------------------------------------------------------------------------------- /webapp/server/README.md: -------------------------------------------------------------------------------- 1 | # Backend 2 | 3 | ## Install prerequisites 4 | 5 | - Node.js version 12.13.1 and higher 6 | - NPM version 6 or higher 7 | - [nodemon](https://nodemon.io/) 8 | 9 | ## Update the .env file 10 | 11 | The demo is set up with a Hyperledger Fabric v2 network deployed on Chainstack, replace the following values `MSP_ID`, `ORDERER_NAME` and `PEER_NAME` with the Hyperledger Fabric network details from Chainstack console. 12 | 13 | ```bash 14 | AS_LOCALHOST=false 15 | 16 | CHANNEL_ID=defaultchannel 17 | CHAINCODE_NAME=freedomDividendContract 18 | CHAINCODE_VERSION=1 19 | CHAINCODE_SEQUENCE=1 20 | 21 | # Org 22 | MSP_ID=Org1MSP 23 | 24 | # Orderer 25 | ORDERER_NAME=orderer.example.com 26 | 27 | # Peer 28 | PEER_NAME=peer0.org1.example.com 29 | 30 | ``` 31 | 32 | ## Build setup 33 | 34 | ### Step 1: install Hyperledger Fabric binaries 35 | 36 | ```bash 37 | ### mac 38 | bash downloadPeerBinary.sh 39 | 40 | ### linux 41 | bash downloadPeerBinary.sh linux 42 | ``` 43 | 44 | ### Step 2: install dependencies 45 | 46 | ```bash 47 | cd /webapp/server 48 | npm install 49 | ``` 50 | 51 | ### Step 3: start Node.js server 52 | 53 | ```bash 54 | ### nodemon 55 | npm run dev 56 | 57 | ### node 58 | npm run start 59 | ``` 60 | -------------------------------------------------------------------------------- /webapp/client/src/App.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 61 | -------------------------------------------------------------------------------- /contract/transaction_data/freedom-dividend-transactions.txdata: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "transactionName": "freedomDividendExists", 4 | "transactionLabel": "A test freedomDividendExists transaction", 5 | "arguments": [ 6 | "001" 7 | ], 8 | "transientData": {} 9 | }, 10 | { 11 | "transactionName": "createFreedomDividend", 12 | "transactionLabel": "A test createFreedomDividend transaction", 13 | "arguments": [ 14 | "001", 15 | "some value" 16 | ], 17 | "transientData": {} 18 | }, 19 | { 20 | "transactionName": "readFreedomDividend", 21 | "transactionLabel": "A test readFreedomDividend transaction", 22 | "arguments": [ 23 | "001" 24 | ], 25 | "transientData": {} 26 | }, 27 | { 28 | "transactionName": "updateFreedomDividend", 29 | "transactionLabel": "A test updateFreedomDividend transaction", 30 | "arguments": [ 31 | "001", 32 | "some other value" 33 | ], 34 | "transientData": {} 35 | }, 36 | { 37 | "transactionName": "deleteFreedomDividend", 38 | "transactionLabel": "A test deleteFreedomDividend transaction", 39 | "arguments": [ 40 | "001" 41 | ], 42 | "transientData": {} 43 | } 44 | ] 45 | -------------------------------------------------------------------------------- /webapp/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fabricWebApp", 3 | "version": "1.0.0", 4 | "description": "Fabric web app backend application", 5 | "main": "app.js", 6 | "scripts": { 7 | "preinstall": "npx npm-force-resolutions", 8 | "start": "babel-node ./app.js", 9 | "dev": "nodemon --exec babel-node ./app.js" 10 | }, 11 | "author": "Thomas Lee @Chainstack", 12 | "license": "MIT", 13 | "engines": { 14 | "node": ">=8", 15 | "npm": ">=5" 16 | }, 17 | "engineStrict": true, 18 | "dependencies": { 19 | "body-parser": "^1.19.0", 20 | "connect-history-api-fallback": "^1.6.0", 21 | "debug": "^4.1.1", 22 | "express": "^4.17.1", 23 | "fabric-network": "^2.1.0", 24 | "history": "^4.10.1", 25 | "ini": "^1.3.6", 26 | "js-yaml": "^3.13.1", 27 | "serve-static": "^1.14.1" 28 | }, 29 | "devDependencies": { 30 | "@babel/core": "^7.7.7", 31 | "@babel/node": "^7.7.7", 32 | "@babel/preset-env": "^7.7.7", 33 | "babel-plugin-module-resolver": "^3.2.0", 34 | "envfile": "^5.0.0", 35 | "jsrsasign": "^10.5.25", 36 | "rimraf": "^3.0.2", 37 | "minimist": "^1.2.6" 38 | }, 39 | "resolutions": { 40 | "ini": "^1.3.6", 41 | "jsrsasign": "^8.0.24", 42 | "minimist": "^1.2.5" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /webapp/server/app.js: -------------------------------------------------------------------------------- 1 | import api from './api'; 2 | import bodyParser from 'body-parser'; 3 | import express from 'express'; 4 | import history from 'connect-history-api-fallback'; 5 | import path from 'path'; 6 | import serveStatic from 'serve-static'; 7 | import { unlockScriptFolder } from '/cli'; 8 | import { register } from '/fabric/wallet'; 9 | import { connect } from '/fabric/gateway'; 10 | 11 | const setupFabricWalletAndGateway = async () => { 12 | unlockScriptFolder(); 13 | 14 | // sample implementation of Fabric SDK gateway and wallet 15 | console.log('Setting up fabric wallet and gateway...'); 16 | await register('user01'); 17 | await connect('user01'); 18 | console.log('Set up complete!'); 19 | } 20 | 21 | setupFabricWalletAndGateway(); 22 | const app = express(); 23 | 24 | const historyMiddleware = history({ 25 | disableDotRule: true, 26 | verbose: true 27 | }); 28 | 29 | const staticFileMiddleware = express.static(path.join(__dirname + '/../dist')); 30 | app.use(staticFileMiddleware); 31 | app.use((req, res, next) => { 32 | if (req.path.includes('api/v1/')) { 33 | next(); 34 | } else { 35 | historyMiddleware(req, res, next); 36 | } 37 | }); 38 | app.use(staticFileMiddleware); 39 | 40 | app.use(serveStatic(__dirname + '/../dist')); 41 | const port = process.env.PORT || 4000; 42 | const hostname = 'localhost'; 43 | 44 | app.use(bodyParser.json()); 45 | app.use('/api/v1', api); 46 | 47 | app.listen(port, hostname, () => { 48 | console.log(`Server running at http://${hostname}:${port}/`); 49 | }); 50 | -------------------------------------------------------------------------------- /webapp/server/cli/peer.js: -------------------------------------------------------------------------------- 1 | const { rootPath } = require('../fabric/utils/helper'); 2 | const { execute, unlockScriptFolder } = require('./index'); 3 | const envfile = require('envfile'); 4 | const fs = require('fs'); 5 | 6 | // set .env CONTRACT_VERSION and CONTRACT_SEQUENCE 7 | const setContractVersion = (upgrade = false) => { 8 | const parsedFile = envfile.parseFileSync(`${rootPath}/webapp/server/.env`); 9 | parsedFile.CHAINCODE_VERSION = upgrade ? (parseFloat(parsedFile.CHAINCODE_VERSION) + 0.1).toFixed(1) : 1.0; 10 | parsedFile.CHAINCODE_SEQUENCE = upgrade ? parseFloat(parsedFile.CHAINCODE_SEQUENCE) + 1 : 1; 11 | 12 | fs.writeFileSync(`${rootPath}/webapp/server/.env`, envfile.stringifySync(parsedFile)); 13 | }; 14 | 15 | const main = async () => { 16 | const [action] = process.argv.slice(2); 17 | if (!['upgrade', 'install'].includes(action)) { 18 | console.log('Error: Invalid argument'); 19 | console.log('node contract upgrade or contract install'); 20 | 21 | return; 22 | } 23 | console.log(`executing cli - ${action} command`); 24 | 25 | unlockScriptFolder(); 26 | 27 | if (action === 'install') { 28 | setContractVersion(false); 29 | } 30 | 31 | if (action === 'upgrade') { 32 | setContractVersion(true); 33 | } 34 | 35 | execute({ ACTION: action }) 36 | .then(({ stdout, stderr }) => { 37 | console.log(stdout); 38 | console.log(stderr); 39 | }) 40 | .catch(({ stdout, stderr }) => { 41 | console.log(stdout); 42 | console.log(stderr); 43 | }); 44 | }; 45 | 46 | 47 | main(); 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Freedom dividend chaincode 2 | 3 | This project contains the web app that we will be spinnnig and connecting to the Hyperledger Fabric network deployed on [Chainstack](https://chainstack.com). 4 | 5 | This is a two-part project: 6 | 7 | * [Web app](https://chainstack.com/deploy-a-hyperledger-fabric-v2-web-app-using-sdk-for-node-js/) in the `webapp` directory. 8 | * [Contract](https://docs.chainstack.com/tutorials/fabric/universal-basic-income-opt-in-chaincode#universal-basic-income-opt-in-chaincode) in the `contract` directory. 9 | 10 | ## Contract 11 | 12 | - Includes a sample JavaScript chaincode with 3 transactions: 13 | - optIn 14 | - optOut 15 | - querySSN 16 | 17 | ## Web app 18 | 19 | ### Backend 20 | 21 | #### Highlights 22 | 23 | - Includes a bash script that automates peer chaincode lifecycle for installing and upgrading chaincodes. 24 | - Includes an API endpoint to bridge Node.js and Hyperledger Fabric v2 network using the bash script. 25 | - Includes a sample implementation of Hyperledger Fabric SDK v2.1 for creating gateway and wallets. 26 | 27 | [Build setup](./webapp/server/README.md) 28 | 29 | By default, this application is configured to work out of the box with a Hyperledger Fabric v2 network deployed on Chainstack, but by 30 | doing minor changes you can easily switch to a network deployed on your local machine. 31 | 32 | ### Frontend 33 | 34 | #### Highlights 35 | 36 | - Automatically retrieves and displays the installed packages and chaincode from the backend. 37 | - Automatically generates forms based on installed chaincode. 38 | 39 | [Build setup](./webapp/client/README.md) 40 | -------------------------------------------------------------------------------- /contract/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "freedomDividendContract", 3 | "version": "1.0.2", 4 | "description": "Freedom Dividend Contract", 5 | "main": "index.js", 6 | "engines": { 7 | "node": ">=8", 8 | "npm": ">=5" 9 | }, 10 | "scripts": { 11 | "preinstall": "npx npm-force-resolutions", 12 | "lint": "eslint .", 13 | "pretest": "npm run lint", 14 | "test": "nyc mocha --recursive", 15 | "start": "fabric-chaincode-node start" 16 | }, 17 | "engineStrict": true, 18 | "author": "Ake Gaviar @Chainstack", 19 | "license": "MIT", 20 | "dependencies": { 21 | "class-transformer": ">=0.3.1", 22 | "debug": "^4.1.1", 23 | "fabric-contract-api": "^2.2.0", 24 | "fabric-shim": "^2.2.0" 25 | }, 26 | "devDependencies": { 27 | "acorn": "^7.1.1", 28 | "chai": "^4.2.0", 29 | "chai-as-promised": "^7.1.1", 30 | "eslint": "^6.3.0", 31 | "ini": "^1.3.6", 32 | "minimist": "^1.2.6", 33 | "mocha": "^6.2.3", 34 | "nyc": "^14.1.1", 35 | "sinon": "^7.4.1", 36 | "sinon-chai": "^3.5.0", 37 | "winston": "^3.2.1", 38 | "yargs-parser": "^13.1.2" 39 | }, 40 | "resolutions": { 41 | "acorn": "^7.1.1", 42 | "class-transformer": ">=0.3.1", 43 | "ini": "^1.3.6", 44 | "minimist": "^1.2.5", 45 | "yargs-parser": "^13.1.2" 46 | }, 47 | "nyc": { 48 | "exclude": [ 49 | ".eslintrc.js", 50 | "coverage/**", 51 | "test/**" 52 | ], 53 | "reporter": [ 54 | "text-summary", 55 | "html" 56 | ], 57 | "all": true, 58 | "check-coverage": true, 59 | "statements": 100, 60 | "branches": 100, 61 | "functions": 100, 62 | "lines": 100 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /webapp/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fabricWebApp", 3 | "description": "Fabric web app frontend application", 4 | "version": "0.1.0", 5 | "private": true, 6 | "scripts": { 7 | "preinstall": "npx npm-force-resolutions", 8 | "serve": "vue-cli-service serve", 9 | "build": "vue-cli-service build", 10 | "lint": "vue-cli-service lint" 11 | }, 12 | "author": "Thomas Lee @Chainstack", 13 | "license": "MIT", 14 | "dependencies": { 15 | "@mdi/font": "^3.6.95", 16 | "axios": "^0.21.2", 17 | "core-js": "^3.6.4", 18 | "highlight.js": "10.4.1", 19 | "ini": "^1.3.6", 20 | "is-svg": "^4.2.2", 21 | "roboto-fontface": "*", 22 | "sass-loader": "^10.1.1", 23 | "ssri": "^8.0.1", 24 | "vue": "^2.6.11", 25 | "vue-json-tree-view": "^2.1.6", 26 | "vue-router": "^3.1.5", 27 | "vuetify": "^2.2.11" 28 | }, 29 | "devDependencies": { 30 | "@vue/cli-plugin-babel": "~4.2.0", 31 | "@vue/cli-plugin-eslint": "~4.2.0", 32 | "@vue/cli-plugin-router": "~4.2.0", 33 | "@vue/cli-service": "~4.2.0", 34 | "@vue/eslint-config-airbnb": "^5.0.2", 35 | "babel-eslint": "^10.0.3", 36 | "eslint": "^6.7.2", 37 | "eslint-plugin-import": "^2.20.1", 38 | "eslint-plugin-vue": "^6.1.2", 39 | "sass": "^1.54.5", 40 | "vue-cli-plugin-vuetify": "~2.0.5", 41 | "vue-template-compiler": "^2.6.11" 42 | }, 43 | "resolutions": { 44 | "highlight.js": "10.4.1", 45 | "ini": "^1.3.6", 46 | "is-svg": ">=4.2.2", 47 | "ssri": "^8.0.1" 48 | }, 49 | "eslintConfig": { 50 | "root": true, 51 | "env": { 52 | "node": true 53 | }, 54 | "extends": [ 55 | "plugin:vue/essential", 56 | "@vue/airbnb" 57 | ], 58 | "parserOptions": { 59 | "parser": "babel-eslint" 60 | }, 61 | "rules": {} 62 | }, 63 | "browserslist": [ 64 | "> 1%", 65 | "last 2 versions" 66 | ] 67 | } 68 | -------------------------------------------------------------------------------- /webapp/client/src/components/ActionButton.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 88 | 89 | 94 | -------------------------------------------------------------------------------- /contract/contract-metadata/metadata-sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://fabric-shim.github.io/master/contract-schema.json", 3 | "contracts": { 4 | "FreedomDividendContract": { 5 | "name": "FreedomDividendContract", 6 | "contractInstance": { 7 | "name": "FreedomDividendContract", 8 | "default": true 9 | }, 10 | "transactions": [ 11 | { 12 | "name": "optIn", 13 | "tags": [ 14 | "submitTx" 15 | ], 16 | "parameters": [ 17 | { 18 | "name": "ssnId", 19 | "description": "SSN ID", 20 | "schema": { 21 | "type": "string" 22 | } 23 | }, 24 | { 25 | "name": "description", 26 | "description": "description", 27 | "schema": { 28 | "type": "string" 29 | } 30 | } 31 | ] 32 | }, 33 | { 34 | "name": "optOut", 35 | "tags": [ 36 | "submitTx" 37 | ], 38 | "parameters": [ 39 | { 40 | "name": "ssnId", 41 | "description": "SSN ID", 42 | "schema": { 43 | "type": "string" 44 | } 45 | } 46 | ] 47 | }, 48 | { 49 | "name": "querySSN", 50 | "tags": [ 51 | "submitTx" 52 | ], 53 | "parameters": [ 54 | { 55 | "name": "ssnId", 56 | "description": "SSN ID", 57 | "schema": { 58 | "type": "string" 59 | } 60 | } 61 | ] 62 | } 63 | ], 64 | "info": { 65 | "title": "", 66 | "version": "" 67 | } 68 | }, 69 | "org.hyperledger.fabric": { 70 | "name": "org.hyperledger.fabric", 71 | "contractInstance": { 72 | "name": "org.hyperledger.fabric" 73 | }, 74 | "transactions": [ 75 | { 76 | "name": "GetMetadata" 77 | } 78 | ], 79 | "info": { 80 | "title": "", 81 | "version": "" 82 | } 83 | } 84 | }, 85 | "info": { 86 | "version": "1.0.2", 87 | "title": "freedomDividendContract" 88 | }, 89 | "components": { 90 | "schemas": {} 91 | } 92 | } -------------------------------------------------------------------------------- /contract/lib/freedomDividendContract.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { Contract} = require('fabric-contract-api'); 3 | 4 | class FreedomDividendContract extends Contract { 5 | 6 | /** Opt in 7 | * 8 | * Let those willing to opt in to the Universal Basic Income program 9 | * provide their Social Security number and an opt-in. 10 | * 11 | * This write the transaction to the ledger and update the world state. 12 | * 13 | * @param ctx - the context of the transaction 14 | * @param ssnId - a Social Security number 15 | * @param optedIn - an opt-in value 16 | */ 17 | async optIn(ctx,ssnId,optedIn) { 18 | 19 | let ssn={ 20 | opt:optedIn, 21 | }; 22 | 23 | await ctx.stub.putState(ssnId,Buffer.from(JSON.stringify(ssn))); 24 | 25 | console.log('This Social Security number has successfully opted in to Freedom Dividend.'); 26 | 27 | } 28 | 29 | /** Opt out 30 | * 31 | * If a citizen has opted in to the Universal Basic Income program, they now 32 | * have the option to opt out. All they need to do is provide their 33 | * Social Security number. 34 | * 35 | * The opt-out is basically a transaction to the ledger that removes the 36 | * existing Social Security number from the world state. 37 | * 38 | * @param ctx - the context of the transaction 39 | * @param ssnId - a Social Security number 40 | */ 41 | async optOut(ctx,ssnId) { 42 | 43 | await ctx.stub.deleteState(ssnId); 44 | 45 | console.log('This Social Security number has successfully opted out of Freedom Dividend.'); 46 | 47 | } 48 | 49 | /** Query a Social Security Number that has opted in 50 | * 51 | * If a citizen has opted in the Universal Basic Income program, their 52 | * Social Security number can now be queried from the world state to 53 | * include them in the monthly Freedom Dividend distribution. 54 | * 55 | * @param ctx - the context of the transaction 56 | * @param ssnId - a Social Security number 57 | */ 58 | async querySSN(ctx,ssnId) { 59 | 60 | let ssnAsBytes = await ctx.stub.getState(ssnId); 61 | if (!ssnAsBytes || ssnAsBytes.toString().length <= 0) { 62 | throw new Error('This Social Security number is not opted in to Freedom Dividend.'); 63 | } 64 | let ssn=JSON.parse(ssnAsBytes.toString()); 65 | 66 | return JSON.stringify(ssn); 67 | } 68 | } 69 | 70 | module.exports=FreedomDividendContract; 71 | -------------------------------------------------------------------------------- /webapp/server/fabric/utils/helper.js: -------------------------------------------------------------------------------- 1 | const envfile = require('envfile'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | 5 | const rootPath = process.cwd().includes('webapp') 6 | ? process.cwd().substring(0, process.cwd().indexOf('webapp')) 7 | : `${process.cwd()}`; 8 | const seralizePath = fileName => fs.readFileSync(path.resolve(__dirname, fileName), 'utf8'); 9 | 10 | const getDirectory = (folderPath) => { 11 | return fs.promises.readdir(folderPath, (err, data) => { 12 | if (err) throw err; 13 | 14 | return data; 15 | }); 16 | }; 17 | 18 | const generateCertPath = async () => { 19 | const { 20 | CHANNEL_ID, 21 | CHAINCODE_NAME, 22 | CHAINCODE_VERSION, 23 | CHAINCODE_SEQUENCE, 24 | ORDERER_NAME, 25 | MSP_ID, 26 | PEER_NAME, 27 | } = envfile.parseFileSync(`${rootPath}/webapp/server/.env`); 28 | const certDirectoryPath = `${rootPath}/webapp/certs`; 29 | 30 | const ADMIN_CERT = await getDirectory(`${certDirectoryPath}/msp/admincerts`).then(([certName]) => { 31 | return `${certDirectoryPath}/msp/admincerts/${certName}`; 32 | }); 33 | const ADMIN_PRIVATE_KEY = `${certDirectoryPath}/msp/keystore/priv_sk`; 34 | const PEER_TLS_ROOTCERT_FILE = await getDirectory(`${certDirectoryPath}/msp/tlscacerts`).then(([certName]) => { 35 | return `${certDirectoryPath}/msp/tlscacerts/${certName}`; 36 | }); 37 | 38 | const ordererName = ORDERER_NAME.split('.').shift(); 39 | 40 | return ({ 41 | ADMIN_CERT, 42 | ADMIN_PRIVATE_KEY, 43 | CHANNEL_ID, 44 | CHAINCODE_NAME, 45 | CHAINCODE_VERSION, 46 | CHAINCODE_SEQUENCE, 47 | ORDERER_CA: `${certDirectoryPath}/${ordererName}-cert.pem`, 48 | ORDERER_ADDRESS: `${ORDERER_NAME}:7050`, 49 | MSP_ID, 50 | MSP_PATH: `${certDirectoryPath}/msp`, 51 | PEER_ADDRESS: `${PEER_NAME}:7051`, 52 | PEER_TLS_ROOTCERT_FILE, 53 | ROOT_PATH: rootPath, 54 | }); 55 | }; 56 | 57 | const flushTmpFolder = () => { 58 | return fs.promises.rmdir(`${rootPath}/webapp/certs/tmp`, { recursive: true }, (err) => { 59 | if (err) { throw err; } 60 | }); 61 | }; 62 | 63 | const makeTmpFolder = async () => { 64 | if (fs.existsSync(`${rootPath}/webapp/certs/tmp`)){ 65 | await flushTmpFolder(); 66 | } 67 | return fs.promises.mkdir(`${rootPath}/webapp/certs/tmp`, (err) => { 68 | if (err) { throw err; } 69 | }); 70 | }; 71 | 72 | module.exports = { 73 | flushTmpFolder, 74 | getDirectory, 75 | generateCertPath, 76 | makeTmpFolder, 77 | seralizePath, 78 | rootPath, 79 | }; 80 | -------------------------------------------------------------------------------- /webapp/client/src/views/Chaincode.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 81 | -------------------------------------------------------------------------------- /webapp/client/src/components/ChaincodeTransactions.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 109 | -------------------------------------------------------------------------------- /contract/test/freedom-dividend-contract.js: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: MIT 3 | */ 4 | 5 | 'use strict'; 6 | 7 | const { ChaincodeStub, ClientIdentity } = require('fabric-shim'); 8 | const { FreedomDividendContract } = require('..'); 9 | const winston = require('winston'); 10 | 11 | const chai = require('chai'); 12 | const chaiAsPromised = require('chai-as-promised'); 13 | const sinon = require('sinon'); 14 | const sinonChai = require('sinon-chai'); 15 | 16 | chai.should(); 17 | chai.use(chaiAsPromised); 18 | chai.use(sinonChai); 19 | 20 | class TestContext { 21 | 22 | constructor() { 23 | this.stub = sinon.createStubInstance(ChaincodeStub); 24 | this.clientIdentity = sinon.createStubInstance(ClientIdentity); 25 | this.logging = { 26 | getLogger: sinon.stub().returns(sinon.createStubInstance(winston.createLogger().constructor)), 27 | setLevel: sinon.stub(), 28 | }; 29 | } 30 | 31 | } 32 | 33 | describe('FreedomDividendContract', () => { 34 | 35 | let contract; 36 | let ctx; 37 | 38 | beforeEach(() => { 39 | contract = new FreedomDividendContract(); 40 | ctx = new TestContext(); 41 | ctx.stub.getState.withArgs('1001').resolves(Buffer.from('{"value":"freedom dividend 1001 value"}')); 42 | ctx.stub.getState.withArgs('1002').resolves(Buffer.from('{"value":"freedom dividend 1002 value"}')); 43 | }); 44 | 45 | describe('#freedomDividendExists', () => { 46 | 47 | it('should return true for a freedom dividend', async () => { 48 | await contract.freedomDividendExists(ctx, '1001').should.eventually.be.true; 49 | }); 50 | 51 | it('should return false for a freedom dividend that does not exist', async () => { 52 | await contract.freedomDividendExists(ctx, '1003').should.eventually.be.false; 53 | }); 54 | 55 | }); 56 | 57 | describe('#createFreedomDividend', () => { 58 | 59 | it('should create a freedom dividend', async () => { 60 | await contract.createFreedomDividend(ctx, '1003', 'freedom dividend 1003 value'); 61 | ctx.stub.putState.should.have.been.calledOnceWithExactly('1003', Buffer.from('{"value":"freedom dividend 1003 value"}')); 62 | }); 63 | 64 | it('should throw an error for a freedom dividend that already exists', async () => { 65 | await contract.createFreedomDividend(ctx, '1001', 'myvalue').should.be.rejectedWith(/The freedom dividend 1001 already exists/); 66 | }); 67 | 68 | }); 69 | 70 | describe('#readFreedomDividend', () => { 71 | 72 | it('should return a freedom dividend', async () => { 73 | await contract.readFreedomDividend(ctx, '1001').should.eventually.deep.equal({ value: 'freedom dividend 1001 value' }); 74 | }); 75 | 76 | it('should throw an error for a freedom dividend that does not exist', async () => { 77 | await contract.readFreedomDividend(ctx, '1003').should.be.rejectedWith(/The freedom dividend 1003 does not exist/); 78 | }); 79 | 80 | }); 81 | 82 | describe('#updateFreedomDividend', () => { 83 | 84 | it('should update a freedom dividend', async () => { 85 | await contract.updateFreedomDividend(ctx, '1001', 'freedom dividend 1001 new value'); 86 | ctx.stub.putState.should.have.been.calledOnceWithExactly('1001', Buffer.from('{"value":"freedom dividend 1001 new value"}')); 87 | }); 88 | 89 | it('should throw an error for a freedom dividend that does not exist', async () => { 90 | await contract.updateFreedomDividend(ctx, '1003', 'freedom dividend 1003 new value').should.be.rejectedWith(/The freedom dividend 1003 does not exist/); 91 | }); 92 | 93 | }); 94 | 95 | describe('#deleteFreedomDividend', () => { 96 | 97 | it('should delete a freedom dividend', async () => { 98 | await contract.deleteFreedomDividend(ctx, '1001'); 99 | ctx.stub.deleteState.should.have.been.calledOnceWithExactly('1001'); 100 | }); 101 | 102 | it('should throw an error for a freedom dividend that does not exist', async () => { 103 | await contract.deleteFreedomDividend(ctx, '1003').should.be.rejectedWith(/The freedom dividend 1003 does not exist/); 104 | }); 105 | 106 | }); 107 | 108 | }); -------------------------------------------------------------------------------- /webapp/client/public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /webapp/client/public/chainstackLogo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /webapp/server/cli/scripts/chaincode.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | trap 'last_command=$current_command; current_command=$BASH_COMMAND' DEBUG 5 | 6 | FABRIC_PATH="$(dirname "$0")/../../../../hlf" 7 | 8 | export FABRIC_BIN_PATH="${FABRIC_PATH}/bin" 9 | export FABRIC_CFG_PATH="${FABRIC_PATH}/config" 10 | 11 | export CORE_PEER_TLS_ENABLED=true 12 | export CORE_PEER_ADDRESS=$PEER_ADDRESS 13 | export CORE_PEER_LOCALMSPID=$MSP_ID 14 | export CORE_PEER_MSPCONFIGPATH=$MSP_PATH 15 | export CORE_PEER_TLS_ROOTCERT_FILE=$PEER_TLS_ROOTCERT_FILE 16 | 17 | discoverPeers() { 18 | ${FABRIC_BIN_PATH}/discover \ 19 | --peerTLSCA "$PEER_TLS_ROOTCERT_FILE" \ 20 | --userKey "$ADMIN_PRIVATE_KEY" \ 21 | --userCert "$ADMIN_CERT" \ 22 | --MSP "$MSP_ID" \ 23 | peers --server "$PEER_ADDRESS" \ 24 | --channel "$CHANNEL_ID" 25 | } 26 | 27 | discoverConfig() { 28 | ${FABRIC_BIN_PATH}/discover \ 29 | --peerTLSCA "$PEER_TLS_ROOTCERT_FILE" \ 30 | --userKey "$ADMIN_PRIVATE_KEY" \ 31 | --userCert "$ADMIN_CERT" \ 32 | --MSP "$MSP_ID" \ 33 | config --server "$PEER_ADDRESS" \ 34 | --channel "$CHANNEL_ID" 35 | } 36 | 37 | installChaincode() { 38 | ${FABRIC_BIN_PATH}/peer lifecycle chaincode package "${ROOT_PATH}/${CHAINCODE_NAME}.tar.gz" \ 39 | --lang node \ 40 | --path "${ROOT_PATH}/contract" \ 41 | --label "${CHAINCODE_NAME}${CHAINCODE_VERSION}" 42 | ${FABRIC_BIN_PATH}/peer lifecycle chaincode install "${ROOT_PATH}/${CHAINCODE_NAME}.tar.gz" 43 | } 44 | 45 | getChaincodePackageID() { 46 | PACKAGES=$(${FABRIC_BIN_PATH}/peer lifecycle chaincode queryinstalled | grep "${CHAINCODE_NAME}${CHAINCODE_VERSION}":) 47 | PACKAGE_ID=${PACKAGES#*Package ID: } 48 | export PACKAGE_ID=${PACKAGE_ID%,*} 49 | 50 | echo "PACKAGE_ID:" ${PACKAGE_ID} 51 | } 52 | 53 | approveChaincode() { 54 | ${FABRIC_BIN_PATH}/peer lifecycle chaincode approveformyorg \ 55 | --name "$CHAINCODE_NAME" \ 56 | --package-id "$PACKAGE_ID" -o "$ORDERER_ADDRESS" \ 57 | --tls \ 58 | --tlsRootCertFiles "$PEER_TLS_ROOTCERT_FILE" \ 59 | --cafile "$ORDERER_CA" \ 60 | --version "$CHAINCODE_VERSION" \ 61 | --channelID "$CHANNEL_ID" \ 62 | --sequence "$CHAINCODE_SEQUENCE" 63 | # --init-required \ 64 | } 65 | 66 | checkReadiness() { 67 | ${FABRIC_BIN_PATH}/peer lifecycle chaincode checkcommitreadiness -o "$ORDERER_ADDRESS" \ 68 | --channelID "$CHANNEL_ID" \ 69 | --tls \ 70 | --cafile "$ORDERER_CA" \ 71 | --name "$CHAINCODE_NAME" \ 72 | --version "$CHAINCODE_VERSION" \ 73 | --sequence "$CHAINCODE_SEQUENCE" \ 74 | --output "${OUTPUT}" 75 | # --init-required \ 76 | } 77 | 78 | commitChaincode() { 79 | PEER_ADDRESSES_LIST=(${PEER_ADDRESSES}) && 80 | TLS_ROOTCERT_FILES_LIST=(${TLS_ROOTCERT_FILES}) && 81 | ${FABRIC_BIN_PATH}/peer lifecycle chaincode commit -o "$ORDERER_ADDRESS" \ 82 | --channelID "$CHANNEL_ID" \ 83 | --name "$CHAINCODE_NAME" \ 84 | --version "$CHAINCODE_VERSION" \ 85 | --sequence "$CHAINCODE_SEQUENCE" \ 86 | --tls \ 87 | --cafile "$ORDERER_CA" \ 88 | ${PEER_ADDRESSES_LIST[@]/#/ --peerAddresses } \ 89 | ${TLS_ROOTCERT_FILES_LIST[@]/#/ --tlsRootCertFiles } 90 | # --init-required \ 91 | } 92 | 93 | queryInstalled() { 94 | ${FABRIC_BIN_PATH}/peer lifecycle chaincode queryinstalled \ 95 | --output "${OUTPUT}" 96 | } 97 | 98 | queryCommitted() { 99 | ${FABRIC_BIN_PATH}/peer lifecycle chaincode querycommitted -o "$ORDERER_ADDRESS" \ 100 | --channelID "$CHANNEL_ID" \ 101 | --tls \ 102 | --cafile "$ORDERER_CA" \ 103 | --peerAddresses "$PEER_ADDRESS" \ 104 | --tlsRootCertFiles "$PEER_TLS_ROOTCERT_FILE" \ 105 | --output "${OUTPUT}" 106 | } 107 | 108 | queryApproved() { 109 | ${FABRIC_BIN_PATH}/peer lifecycle chaincode queryapproved -o "$ORDERER_ADDRESS" \ 110 | --channelID "$CHANNEL_ID" \ 111 | --name "$CHAINCODE_NAME" \ 112 | --output "${OUTPUT}" 113 | } 114 | 115 | invokeChaincode() { 116 | PEER_ADDRESSES_LIST=(${PEER_ADDRESSES}) && 117 | TLS_ROOTCERT_FILES_LIST=(${TLS_ROOTCERT_FILES}) && 118 | ${FABRIC_BIN_PATH}/peer chaincode invoke -o "$ORDERER_ADDRESS" \ 119 | --tls \ 120 | --cafile "$ORDERER_CA" \ 121 | --channelID "$CHANNEL_ID" \ 122 | --name "$CHAINCODE_NAME" \ 123 | ${PEER_ADDRESSES_LIST[@]/#/ --peerAddresses } \ 124 | ${TLS_ROOTCERT_FILES_LIST[@]/#/ --tlsRootCertFiles } \ 125 | -c "{\"Args\": ${ARGS}}" 126 | } 127 | 128 | OUTPUT="plain-text" 129 | if [[ $ACTION == "invoke" ]] 130 | then 131 | invokeChaincode 132 | elif [[ $ACTION == "install" ]] 133 | then 134 | installChaincode 135 | elif [[ $ACTION == "upgrade" ]] 136 | then 137 | installChaincode 138 | elif [[ $ACTION == "approve" ]] 139 | then 140 | approveChaincode 141 | elif [[ $ACTION == "commit" ]] 142 | then 143 | commitChaincode 144 | elif [[ $ACTION == "queryCommitted" ]] 145 | then 146 | OUTPUT="json" 147 | queryCommitted 148 | elif [[ $ACTION == "queryInstalled" ]] 149 | then 150 | OUTPUT="json" 151 | queryInstalled 152 | elif [[ $ACTION == "queryApproved" ]] 153 | then 154 | OUTPUT="json" 155 | queryApproved 156 | elif [[ $ACTION == "checkReadiness" ]] 157 | then 158 | OUTPUT="json" 159 | checkReadiness 160 | elif [[ $ACTION == "discoverConfig" ]] 161 | then 162 | discoverConfig 163 | elif [[ $ACTION == "discoverPeers" ]] 164 | then 165 | discoverPeers 166 | else 167 | echo "invalid action - ${ACTION}" 168 | fi 169 | -------------------------------------------------------------------------------- /webapp/client/src/views/Main.vue: -------------------------------------------------------------------------------- 1 | 108 | 109 | 165 | 166 | 199 | -------------------------------------------------------------------------------- /webapp/server/api/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { gateway } from 'fabric/gateway'; 3 | import { execute } from 'cli'; 4 | const fs = require('fs'); 5 | const api = express(); 6 | const envfile = require('envfile'); 7 | const { flushTmpFolder, makeTmpFolder, rootPath } = require('../fabric/utils/helper'); 8 | 9 | const getPeersAndTLSRootCerts = async (msps, peers, filterList = null) => { 10 | const PEER_ADDRESSES = []; 11 | const TLS_ROOTCERT_FILES = []; 12 | 13 | await makeTmpFolder(); 14 | // retrieve peerAddresses and tlsRootCertFiles of approved organizations 15 | for (const { Endpoint, MSPID } of peers) { 16 | if (filterList && filterList.includes(MSPID) || !filterList) { 17 | const filePath = `${rootPath}/webapp/certs/tmp/${MSPID}.pem`; 18 | PEER_ADDRESSES.push(Endpoint); 19 | 20 | await fs.promises.writeFile(filePath, msps[MSPID].tls_root_certs[0], { encoding: 'base64' }) 21 | .then(() => TLS_ROOTCERT_FILES.push(filePath)); 22 | } 23 | } 24 | 25 | return { 26 | PEER_ADDRESSES: PEER_ADDRESSES.join(' '), 27 | TLS_ROOTCERT_FILES: TLS_ROOTCERT_FILES.join(' '), 28 | }; 29 | }; 30 | 31 | api.get('/channel/discovery', async (req, res, next) => { 32 | execute({ ACTION: 'discovery' }).then(({ stdout })=> { 33 | res.send(stdout); 34 | }).catch(({ stderr }) => { 35 | res.status(500).json({ message: stderr }); 36 | }); 37 | }); 38 | 39 | api.get('/network', (req, res, next) => { 40 | Promise.all([ 41 | execute({ ACTION: 'queryInstalled' }), 42 | execute({ ACTION: 'queryCommitted' }), 43 | execute({ ACTION: 'discoverConfig' }), 44 | execute({ ACTION: 'discoverPeers' }), 45 | ]).then(([installed, committed, config, peers]) => { 46 | return { 47 | installed_chaincodes: JSON.parse(installed.stdout).installed_chaincodes, 48 | chaincode_definitions: JSON.parse(committed.stdout).chaincode_definitions, 49 | config: JSON.parse(config.stdout), 50 | peers: JSON.parse(peers.stdout), 51 | mspId: envfile.parseFileSync(`${rootPath}/webapp/server/.env`).MSP_ID, 52 | }; 53 | }) 54 | .then(async (data) => { 55 | if (data.installed_chaincodes) { 56 | let committed = null; 57 | const { CHAINCODE_NAME, CHAINCODE_VERSION } = envfile.parseFileSync(`${rootPath}/webapp/server/.env`); 58 | const activeLabel = `${CHAINCODE_NAME}${CHAINCODE_VERSION}`; 59 | 60 | const activeChaincode = data.installed_chaincodes.find(({ label }) => { 61 | return label === activeLabel; 62 | }); 63 | 64 | if(activeChaincode) { 65 | const { package_id, label, references } = activeChaincode; 66 | committed = data.chaincode_definitions.find(({ name, version }) => { 67 | return `${name}${version}` === activeLabel; 68 | }); 69 | 70 | activeChaincode.details = committed; 71 | activeChaincode.committed = committed !== undefined; 72 | 73 | if (!committed) { 74 | await execute({ ACTION: 'checkReadiness' }) 75 | .then(({ stdout })=> { 76 | activeChaincode.details = { approvals: JSON.parse(stdout).approvals }; 77 | }); 78 | } 79 | 80 | data.installed_chaincodes = [activeChaincode]; 81 | } else { 82 | data.installed_chaincodes = []; 83 | } 84 | } 85 | 86 | res.send(data); 87 | }) 88 | .catch(next); 89 | }); 90 | 91 | api.get('/chaincode/:chaincode', async (req, res, next) => { 92 | try { 93 | const { CHANNEL_ID } = envfile.parseFileSync(`${rootPath}/webapp/server/.env`); 94 | 95 | const network = await gateway.getNetwork(CHANNEL_ID); 96 | const contract = await network.getContract(req.params.chaincode); 97 | const response = await contract.evaluateTransaction('org.hyperledger.fabric:GetMetadata').then((data) => { 98 | const contractData = JSON.parse(data.toString()); 99 | 100 | res.send({ 101 | mspId: envfile.parseFileSync(`${rootPath}/webapp/server/.env`).MSP_ID, 102 | contract: contractData, 103 | }); 104 | }); 105 | } catch(e) { 106 | res.status(500).json(e.message); 107 | } 108 | }); 109 | 110 | // peer cli chaincode binary implementation of invoking chaincode 111 | api.post('/chaincode/transaction', async (req, res, next) => { 112 | Promise.all([ 113 | execute({ ACTION: 'discoverConfig' }), 114 | execute({ ACTION: 'discoverPeers' }), 115 | ]).then(async ([channelConfig, channelPeers]) => { 116 | const { msps } = JSON.parse(channelConfig.stdout); 117 | const peers = JSON.parse(channelPeers.stdout); 118 | const { PEER_ADDRESSES, TLS_ROOTCERT_FILES } = await getPeersAndTLSRootCerts(msps, peers); 119 | 120 | execute({ 121 | ACTION: 'invoke', 122 | PEER_ADDRESSES, 123 | TLS_ROOTCERT_FILES, 124 | ARGS: JSON.stringify(req.body.args), 125 | }).then((response)=> { 126 | flushTmpFolder(); 127 | console.log(response); 128 | res.send(response.stderr); 129 | }).catch(({ stderr }) => { 130 | res.status(500).json(stderr); 131 | }); 132 | }).catch(next); 133 | }); 134 | 135 | // sdk implementation of invoking chaincode 136 | // api.post('/chaincode/transaction', async (req, res, next) => { 137 | // try { 138 | // const channels = Array.from(gateway.client.channels.keys()); 139 | 140 | // const network = await gateway.getNetwork(channels[0]); 141 | // const contract = await network.getContract(req.body.contract); 142 | // const response = await contract.submitTransaction(...req.body.args); 143 | 144 | // res.send(response.toString()); 145 | // } catch(e) { 146 | // res.status(500).json(e.message); 147 | // } 148 | // }); 149 | 150 | api.post('/chaincode/install', async (req, res, next) => { 151 | execute({ ACTION: 'install' }).then(({ stdout })=> { 152 | res.send({ 153 | data: stdout, 154 | }); 155 | }).catch(({ stderr }) => { 156 | res.status(500).json({ message: stderr }); 157 | }); 158 | }); 159 | 160 | api.post('/chaincode/approve', async (req, res, next) => { 161 | execute({ ACTION: 'approve', PACKAGE_ID: req.body.package_id }).then(({ stdout })=> { 162 | res.send(stdout); 163 | }).catch(({ stderr }) => { 164 | res.status(500).json({ message: stderr }); 165 | }); 166 | }); 167 | 168 | api.post('/chaincode/commit', async (req, res, next) => { 169 | Promise.all([ 170 | execute({ ACTION: 'checkReadiness' }), 171 | execute({ ACTION: 'discoverConfig' }), 172 | execute({ ACTION: 'discoverPeers' }), 173 | ]).then(async ([readiness, channelConfig, channelPeers]) => { 174 | const { approvals } = JSON.parse(readiness.stdout); 175 | const { msps } = JSON.parse(channelConfig.stdout); 176 | const peers = JSON.parse(channelPeers.stdout); 177 | const approveMsps = Object.keys(approvals).filter(mspId => approvals[mspId]); 178 | 179 | const { PEER_ADDRESSES, TLS_ROOTCERT_FILES } = await getPeersAndTLSRootCerts(msps, peers, approveMsps); 180 | 181 | execute({ ACTION: 'commit', PEER_ADDRESSES, TLS_ROOTCERT_FILES }).then((response)=> { 182 | flushTmpFolder(); 183 | res.send(response.stdout); 184 | }).catch(({ stderr }) => { 185 | res.status(500).json({ message: stderr }); 186 | }); 187 | }).catch(next); 188 | }); 189 | 190 | export default api; 191 | --------------------------------------------------------------------------------