├── 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 |
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 |
2 |
3 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
24 | {{ alertMessage.message }}
25 |
26 |
27 |
28 |
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 |
2 |
6 |
7 |
14 | {{ display }}
15 | {{ display }}
16 |
17 |
18 | {{ tooltipContent }}
19 |
20 |
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 |
2 |
3 |
4 |
10 |
11 |
12 |
13 | {{ mspId }}
14 |
15 |
Chaincode Name: {{ $route.params.chaincode }}
16 |
17 |
18 |
Contract package info:
19 |
20 | -
21 | {{ key }} : {{ info[key] }}
22 |
23 |
24 |
25 |
26 |
27 | Chaincode transactions:
28 |
29 |
34 |
35 |
36 |
37 |
38 |
81 |
--------------------------------------------------------------------------------
/webapp/client/src/components/ChaincodeTransactions.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
9 |
15 |
16 |
23 | {{ transaction.name }}
24 |
25 |
26 |
27 |
28 |
29 |
39 |
40 |
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 |
16 |
--------------------------------------------------------------------------------
/webapp/client/public/chainstackLogo.svg:
--------------------------------------------------------------------------------
1 |
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 |
2 |
3 |
4 |
10 |
11 |
12 |
13 | {{ mspId }}
14 |
15 | Installed chaincodes
16 |
17 |
23 |
24 |
25 |
26 |
27 |
34 |
35 |
36 |
37 |
41 |
42 |
43 |
44 |
45 |
46 | Package ID: {{ item.package_id }}
47 |
58 |
59 |
69 |
70 |
{{ item.details }}
71 |
72 |
73 | Access Chaincode
74 |
75 |
76 |
77 |
78 |
79 |
80 | Fabric channel details
81 |
82 |
83 |
84 |
90 |
91 |
92 |
93 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
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 |
--------------------------------------------------------------------------------