├── .nvmrc ├── config └── default.json ├── .eslintignore ├── .gitignore ├── permissions.acl ├── .eslintrc.yml ├── package.json ├── test ├── helpers.js └── EngineSupplychainSpec.js ├── models └── EngineSupplychain.cto ├── lib └── logic.js └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | v8.11.3 2 | -------------------------------------------------------------------------------- /config/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": "" 3 | } 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist 3 | go 4 | node_modules 5 | out 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.logs 2 | *.bna 3 | node_modules/ 4 | composer-logs/ 5 | coverage/ 6 | .nyc_output/ 7 | dist/ 8 | -------------------------------------------------------------------------------- /permissions.acl: -------------------------------------------------------------------------------- 1 | rule NetworkAdminUser { 2 | description: "Grant business network administrators full access to user resources" 3 | participant: "org.hyperledger.composer.system.NetworkAdmin" 4 | operation: ALL 5 | resource: "**" 6 | action: ALLOW 7 | } 8 | 9 | rule NetworkAdminSystem { 10 | description: "Grant business network administrators full access to system resources" 11 | participant: "org.hyperledger.composer.system.NetworkAdmin" 12 | operation: ALL 13 | resource: "org.hyperledger.composer.system.**" 14 | action: ALLOW 15 | } 16 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | es6: true 3 | node: true 4 | mocha: true 5 | extends: 'eslint:recommended' 6 | parserOptions: 7 | ecmaVersion: 8 8 | sourceType: 9 | - script 10 | rules: 11 | indent: 12 | - error 13 | - 4 14 | linebreak-style: 15 | - error 16 | - unix 17 | quotes: 18 | - error 19 | - single 20 | semi: 21 | - error 22 | - never 23 | no-unused-vars: 24 | - error 25 | - args: none 26 | no-console: off 27 | curly: error 28 | eqeqeq: error 29 | no-throw-literal: error 30 | no-var: error 31 | dot-notation: error 32 | no-tabs: error 33 | no-trailing-spaces: error 34 | no-use-before-define: error 35 | no-useless-call: error 36 | no-with: error 37 | operator-linebreak: error 38 | yoda: error 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fabric-composer-engine-supplychain", 3 | "version": "0.0.1", 4 | "description": "Tracing engines from production to end customers in a Hyperledger Composer Business Network Definition.", 5 | "scripts": { 6 | "test": "mocha -t 0 --recursive", 7 | "lint": "eslint .", 8 | "createArchive": "npm run lint && npm run test && mkdirp ./dist && composer archive create --sourceType dir --sourceName . -a ./dist/engine-supplychain.bna" 9 | }, 10 | "engines": { 11 | "composer": "0.19.13" 12 | }, 13 | "author": "Jonas Verhoelen", 14 | "email": "jonas.verhoelen@codecentric.de", 15 | "license": "Apache-2.0", 16 | "devDependencies": { 17 | "chai": "^3.5.0", 18 | "composer-admin": "0.19.13", 19 | "composer-cli": "0.19.13", 20 | "composer-client": "0.19.13", 21 | "composer-common": "0.19.13", 22 | "composer-connector-embedded": "0.19.13", 23 | "composer-cucumber-steps": "0.19.13", 24 | "composer-wallet-inmemory": "0.19.13", 25 | "composer-wallet-filesystem": "0.19.13", 26 | "composer-connector-hlfv1": "0.19.13", 27 | "eslint": "^3.6.1", 28 | "istanbul": "^0.4.5", 29 | "mkdirp": "^0.5.1", 30 | "mocha": "^3.2.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test/helpers.js: -------------------------------------------------------------------------------- 1 | function createManufacturer(namespace, factory, id) { 2 | const manu = factory.newResource(namespace, 'Manufacturer', id) 3 | manu.name = 'Some manufacturer for testing' 4 | 5 | return manu 6 | } 7 | 8 | function createMerchant(namespace, factory, id) { 9 | const merchant = factory.newResource(namespace, 'Merchant', id) 10 | merchant.name = 'Some merchant name' 11 | 12 | return merchant 13 | } 14 | 15 | function createEngine(namespace, factory, id, manufacturer) { 16 | const engine = factory.newResource(namespace, 'Engine', id) 17 | const engineProps = factory.newConcept(namespace, 'EngineProperties') 18 | 19 | engineProps.brand = 'Mercedes' 20 | engineProps.model = 'V12' 21 | engineProps.horsePower = 400 22 | engineProps.cubicCapacity = 4000 23 | engineProps.cylindersAmount = 12 24 | 25 | engine.data = engineProps 26 | engine.manufacturer = factory.newRelationship(namespace, 'Manufacturer', manufacturer.$identifier) 27 | 28 | return engine 29 | } 30 | 31 | function createCar(namespace, factory, id) { 32 | const car = factory.newResource(namespace, 'Car', id) 33 | car.legalDocumentId = 'legal-id-of-this-car' 34 | 35 | return car 36 | } 37 | 38 | module.exports = { 39 | createManufacturer, 40 | createMerchant, 41 | createEngine, 42 | createCar 43 | } 44 | -------------------------------------------------------------------------------- /models/EngineSupplychain.cto: -------------------------------------------------------------------------------- 1 | namespace org.acme.enginesupplychain 2 | 3 | /* 4 | * Concepts 5 | */ 6 | 7 | concept EngineProperties { 8 | o String brand 9 | o String model 10 | o Double horsePower 11 | o Double cubicCapacity 12 | o Integer cylindersAmount 13 | } 14 | 15 | concept Address { 16 | o String country 17 | o String city 18 | o String street 19 | o String streetNo 20 | } 21 | 22 | /* 23 | * Assets 24 | */ 25 | 26 | asset Engine identified by engineId { 27 | o String engineId 28 | o EngineProperties data 29 | 30 | --> Manufacturer manufacturer 31 | --> Car currentCar optional 32 | --> Merchant merchant optional 33 | } 34 | 35 | asset Car identified by carId { 36 | o String carId 37 | o String legalDocumentId 38 | } 39 | 40 | /* 41 | * Participants 42 | */ 43 | 44 | participant Member identified by memberId { 45 | o String memberId 46 | o String name 47 | o Address address optional 48 | } 49 | 50 | participant Manufacturer extends Member { 51 | } 52 | 53 | participant Merchant extends Member { 54 | } 55 | 56 | /* 57 | * Transactions 58 | */ 59 | 60 | transaction EngineMerchantTransfer { 61 | --> Engine engine 62 | --> Merchant merchant 63 | } 64 | 65 | transaction EngineCarInstallation { 66 | --> Engine engine 67 | --> Car car 68 | } 69 | 70 | transaction EngineCreation { 71 | --> Manufacturer manufacturer 72 | o EngineProperties data 73 | } 74 | 75 | transaction CarCreation { 76 | o String legalIdDocument 77 | } 78 | 79 | // TODO: transaction logic and unit tests not yet implemented 80 | 81 | // transaction WarrantyClaim { 82 | // --> Engine engine 83 | // --> Car car 84 | // } 85 | 86 | // transaction StolenReport { 87 | // --> Engine engine 88 | // --> Car car 89 | // } 90 | -------------------------------------------------------------------------------- /lib/logic.js: -------------------------------------------------------------------------------- 1 | /* global getAssetRegistry getFactory */ 2 | 3 | const modelsNamespace = 'org.acme.enginesupplychain' 4 | function uuid() { 5 | const s4 = () => Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1) 6 | return `${s4()}${s4()}-${s4()}-${s4()}-${s4()}-${s4()}${s4()}${s4()}` 7 | } 8 | 9 | /** 10 | * Creation of a Engine asset triggered by physical production. 11 | * @param {org.acme.enginesupplychain.EngineCreation} tx - the transaction to create an engine 12 | * @transaction 13 | */ 14 | async function createEngineAsset(tx) { // eslint-disable-line no-unused-vars 15 | const engineRegistry = await getAssetRegistry(modelsNamespace + '.Engine') 16 | const engine = getFactory().newResource(modelsNamespace, 'Engine', uuid()) 17 | const engineData = getFactory().newConcept(modelsNamespace, 'EngineProperties') 18 | 19 | engine.data = Object.assign(engineData, tx.data) 20 | engine.manufacturer = tx.manufacturer 21 | 22 | await engineRegistry.add(engine) 23 | } 24 | 25 | /** 26 | * An engine is transfered to a merchant. 27 | * @param {org.acme.enginesupplychain.EngineMerchantTransfer} tx - the engine transfer transaction 28 | * @transaction 29 | */ 30 | async function transferEngineToMerchant(tx) { // eslint-disable-line no-unused-vars 31 | const engineRegistry = await getAssetRegistry(modelsNamespace + '.Engine') 32 | tx.engine.merchant = tx.merchant 33 | 34 | await engineRegistry.update(tx.engine) 35 | } 36 | 37 | /** 38 | * An engine is installed in a car. 39 | * @param {org.acme.enginesupplychain.EngineCarInstallation} tx - the engine into car installation transaction 40 | * @transaction 41 | */ 42 | async function installEngineToCar(tx) { // eslint-disable-line no-unused-vars 43 | const engineRegistry = await getAssetRegistry(modelsNamespace + '.Engine') 44 | if (tx.car) { 45 | tx.engine.currentCar = tx.car 46 | await engineRegistry.update(tx.engine) 47 | } else { 48 | return Promise.reject('No target car was set on the transaction!') 49 | } 50 | } 51 | 52 | /** 53 | * A car is created 54 | * @param {org.acme.enginesupplychain.CarCreation} tx - transaction to create a new car 55 | * @transaction 56 | */ 57 | async function createCar(tx) { // eslint-disable-line no-unused-vars 58 | const carRegistry = await getAssetRegistry(modelsNamespace + '.Car') 59 | const factory = getFactory() 60 | const carId = uuid() 61 | const car = factory.newResource(modelsNamespace, 'Car', carId) 62 | car.legalDocumentId = tx.legalIdDocument 63 | 64 | await carRegistry.add(car) 65 | } 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Hyperledger Fabric & Composer "Engine Supplychain" example 2 | 3 | This repository defines the source code for a Hyperledger Composer Business Network Definition. The network models a consortium of engine manufacturers and merchants which trace the production, sale and installation of car engines into cars of end customers. 4 | 5 | ## Branching strategy 6 | 7 | Initial and final versions are stored in two separate branches to guide developers throw the process of developing a Business Network Definition for Composer. 8 | This repository was created to demo the engine supplychain application [in a German article for Informatik Aktuell](https://www.informatik-aktuell.de/betrieb/virtualisierung/eine-blockchain-anwendung-mit-hyperledger-fabric-und-composer.html). 9 | 10 | 11 | ### Initial version (default branch) 12 | 13 | In the branch `initial` you will find unfinished application skeleton. It can be started but contains no application code, yet. 14 | 15 | ``` 16 | git checkout initial 17 | ``` 18 | 19 | ### Final version 20 | 21 | In the `master` branch you will find the final solution application. It contains implementations that fulfill a simple engine supplychain show case. 22 | 23 | ``` 24 | git checkout master 25 | ``` 26 | 27 | ## Final functionality covered 28 | 29 | There are different roles/personas: `Manufacturer`, `Merchant` and the interests of a `Customer` by tracing cars by unique legal identifiers of real cars. 30 | 31 | 1. As a `Manufacturer` I want to be able to create produced motors with a unique serial number in order to track further history of it and prove uniqueness. 32 | 2. As a `Manufacturer` I want to be able to transfer ownership of inserted motors to a `Merchant` in order to model a sell proccess to it. 33 | 3. As a `Merchant` I want to be able to transfer an owned motor to a `Customer` in order to model a sell process/installation process into his car. 34 | 35 | ## Further feature ideas 36 | 37 | These are further ideas but did not have been implemented yet. 38 | 39 | 4. As a `Customer` I want to claim the two-year warranty by help of a `Merchant` at a `Manufacturer` in order to get a motors' damage repaired or refunded. 40 | 5. As a `Customer` or `Merchant` or `Manufacturer` I want to report a stolen motor in order to let everyone know that its serial number now resolves to a stolen item. 41 | 42 | ## How to use? 43 | 44 | Pre-requisites: 45 | 46 | 1. Install [pre-requisites for Hyperledger Fabric](http://hyperledger-fabric.readthedocs.io/en/latest/prereqs.html) 47 | 2. Install [pre-requisites for Hyperledger Composer](https://hyperledger.github.io/composer/installing/installing-prereqs) 48 | 3. Install [Composer and its (dev) tools](https://hyperledger.github.io/composer/installing/development-tools) 49 | 50 | The source code is implemented in JavaScript ES6 so it can make use of nice language features such as `async` and `await`, spread/destruct operators and so on. 51 | 52 | Working with the repo: 53 | 54 | 1. Install dependencies and dev-dependencies: `npm install` 55 | 2. Run ESlint: `npm run lint` 56 | 3. Run unit tests: `npm test` 57 | 4. Compile this repository to a `.bna` file: `npm run createArchive` 58 | 59 | For more information on Fabric and Composer and how to deploy `.bna` files to Fabric Networks, please read their development and operations documentation. 60 | -------------------------------------------------------------------------------- /test/EngineSupplychainSpec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const AdminConnection = require('composer-admin').AdminConnection 4 | const BusinessNetworkConnection = require('composer-client').BusinessNetworkConnection 5 | const { BusinessNetworkDefinition, CertificateUtil, IdCard } = require('composer-common') 6 | const path = require('path') 7 | const helpers = require('./helpers') 8 | 9 | require('chai').should() 10 | 11 | const namespace = 'org.acme.enginesupplychain' 12 | 13 | describe('EngineSupplychainSpec', () => { 14 | // In-memory card store for testing so cards are not persisted to the file system 15 | const cardStore = require('composer-common').NetworkCardStoreManager.getCardStore( { type: 'composer-wallet-inmemory' } ) 16 | let adminConnection 17 | let bnc 18 | 19 | // test resources 20 | let testManufacturer 21 | let testMerchant 22 | let testEngine 23 | let testCar 24 | 25 | // and their registries 26 | let manufacturerRegistry 27 | let merchantRegistry 28 | let engineRegistry 29 | let carRegistry 30 | 31 | before(async () => { 32 | // Embedded connection used for local testing 33 | const connectionProfile = { name: 'embedded', 'x-type': 'embedded' } 34 | // Generate certificates for use with the embedded connection 35 | const credentials = CertificateUtil.generate({ commonName: 'admin' }) 36 | 37 | // PeerAdmin identity used with the admin connection to deploy business networks 38 | const deployerMetadata = { version: 1, userName: 'PeerAdmin', roles: [ 'PeerAdmin', 'ChannelAdmin' ] } 39 | const deployerCard = new IdCard(deployerMetadata, connectionProfile) 40 | deployerCard.setCredentials(credentials) 41 | 42 | const deployerCardName = 'PeerAdmin' 43 | adminConnection = new AdminConnection({ cardStore: cardStore }) 44 | 45 | await adminConnection.importCard(deployerCardName, deployerCard) 46 | await adminConnection.connect(deployerCardName) 47 | }) 48 | 49 | beforeEach(async () => { 50 | bnc = new BusinessNetworkConnection({ cardStore: cardStore }) 51 | 52 | const adminUserName = 'admin' 53 | let adminCardName 54 | let businessNetworkDefinition = await BusinessNetworkDefinition.fromDirectory(path.resolve(__dirname, '..')) 55 | 56 | // Install the Composer runtime for the new business network 57 | await adminConnection.install(businessNetworkDefinition) 58 | 59 | // Start the business network and configure an network admin identity 60 | const startOptions = { networkAdmins: [ { userName: adminUserName, enrollmentSecret: 'adminpw' } ] } 61 | const adminCards = await adminConnection.start(businessNetworkDefinition.getName(), businessNetworkDefinition.getVersion(), startOptions) 62 | 63 | // Import the network admin identity for us to use 64 | adminCardName = `${adminUserName}@${businessNetworkDefinition.getName()}` 65 | await adminConnection.importCard(adminCardName, adminCards.get(adminUserName)) 66 | 67 | // Connect to the business network using the network admin identity 68 | await bnc.connect(adminCardName) 69 | 70 | const factory = bnc.getBusinessNetwork().getFactory() 71 | manufacturerRegistry = await bnc.getParticipantRegistry(namespace + '.Manufacturer') 72 | merchantRegistry = await bnc.getParticipantRegistry(namespace + '.Merchant') 73 | engineRegistry = await bnc.getAssetRegistry(namespace + '.Engine') 74 | carRegistry = await bnc.getAssetRegistry(namespace + '.Car') 75 | 76 | testManufacturer = helpers.createManufacturer(namespace, factory, 'test-manufacturer') 77 | testMerchant = helpers.createMerchant(namespace, factory, 'test-merchant') 78 | testEngine = helpers.createEngine(namespace, factory, 'test-engine', testManufacturer) 79 | testCar = helpers.createCar(namespace, factory, 'test-car') 80 | 81 | await manufacturerRegistry.addAll([testManufacturer]) 82 | await merchantRegistry.addAll([testMerchant]) 83 | await engineRegistry.addAll([testEngine]) 84 | await carRegistry.addAll([testCar]) 85 | }) 86 | 87 | describe('createEngineAsset', () => { 88 | it('should create an Engine by submitting a valid EngineCreation transaction', async () => { 89 | const factory = bnc.getBusinessNetwork().getFactory() 90 | 91 | const engineCreationTrans = factory.newTransaction(namespace, 'EngineCreation') 92 | engineCreationTrans.data = factory.newConcept(namespace, 'EngineProperties') 93 | engineCreationTrans.data.brand = 'Audi' 94 | engineCreationTrans.data.model = 'Fancy engine model' 95 | engineCreationTrans.data.horsePower = 400 96 | engineCreationTrans.data.cubicCapacity = 4000 97 | engineCreationTrans.data.cylindersAmount = 10 98 | 99 | const manufacturerRegistry = await bnc.getParticipantRegistry(namespace + '.Manufacturer') 100 | await manufacturerRegistry.addAll([]) 101 | engineCreationTrans.manufacturer = factory.newRelationship(namespace, 'Manufacturer', testManufacturer.$identifier) 102 | 103 | await bnc.submitTransaction(engineCreationTrans) 104 | 105 | const allEngines = await engineRegistry.getAll() 106 | allEngines.length.should.equal(2) 107 | }) 108 | }) 109 | 110 | describe('transferEngineToMerchant', () => { 111 | it('should set the reference to a merchant for an engine', async () => { 112 | const factory = bnc.getBusinessNetwork().getFactory() 113 | 114 | const transferTrans = factory.newTransaction(namespace, 'EngineMerchantTransfer') 115 | transferTrans.merchant = factory.newRelationship(namespace, 'Merchant', testMerchant.$identifier) 116 | transferTrans.engine = factory.newRelationship(namespace, 'Engine', testEngine.$identifier) 117 | 118 | await bnc.submitTransaction(transferTrans) 119 | 120 | const allEngines = await engineRegistry.getAll() 121 | allEngines.length.should.equal(1) 122 | allEngines[0].merchant.$identifier.should.equal(testMerchant.$identifier) 123 | }) 124 | }) 125 | 126 | describe('installEngineToCar', () => { 127 | it('should set the reference to a car for an engine', async () => { 128 | const factory = bnc.getBusinessNetwork().getFactory() 129 | 130 | const installTrans = factory.newTransaction(namespace, 'EngineCarInstallation') 131 | installTrans.engine = factory.newRelationship(namespace, 'Engine', testEngine.$identifier) 132 | installTrans.car = factory.newRelationship(namespace, 'Car', testCar.$identifier) 133 | 134 | await bnc.submitTransaction(installTrans) 135 | 136 | const allEngines = await engineRegistry.getAll() 137 | allEngines.length.should.equal(1) 138 | allEngines[0].currentCar.$identifier.should.equal(testCar.$identifier) 139 | }) 140 | }) 141 | 142 | describe('createCar', () => { 143 | it('should insert a new Car asset', async () => { 144 | const factory = bnc.getBusinessNetwork().getFactory() 145 | 146 | const createCarTrans = factory.newTransaction(namespace, 'CarCreation') 147 | createCarTrans.legalIdDocument = 'some-important-car-id' 148 | 149 | await bnc.submitTransaction(createCarTrans) 150 | 151 | const allCars = await carRegistry.getAll() 152 | allCars.length.should.equal(2) 153 | allCars[0].legalDocumentId.should.equal('some-important-car-id') 154 | allCars[1].legalDocumentId.should.equal('legal-id-of-this-car') 155 | }) 156 | }) 157 | }) 158 | --------------------------------------------------------------------------------