Commit 7fd4f838 authored by Matteo Campanelli's avatar Matteo Campanelli
Browse files

First bulk commit

parent 33eb379e
# Fabric Example Supplychain
Structure of this repo:
- folder `chaincode` contains chaincode (i.e. smart contract logic)
- folder `supplychain` contains the client code to use the smart contract
- folder `supplychain/typescript` contains the client code in typescript
- folder `supplychain/app` contains the web app in flask to use the smart contract
- folder `first-network` contains the basic network configuration for our application (taken from the fabric-samples repo)
To start the application:
- run `./cleanup.sh` (if you already used it in the past)
- `cd supplychain/`
- run `./startFabric.sh` (this may take a few minutes and will compile the chaincode and start the network)
- `cd typescript`
- run `./initchainclient.sh` (this will compile the client code and bootstrap a local wallet)
- run `cd..` and `./runwebclient.sh` (which will start the Flask web server on localhost)
You can then go to `127.0.0.1/5000/user=N` with N = 1,2 or 3 and `127.0.0.1/5000/admin` in your browser.
To compile the client code again you can run `npm run build` from `supplychain/typescript`.
When creating a new app/contract you can (and possibly should) change the "supplychain" string by:
- editing {.,chaincode}/supplychain/typescript/package.json and replacing the "name" field
- changing CONTRACT_NAME in contractConfig.sh
- change contractName and directory in the beginning of supplychain/typescript/src/client.ts
- renaming folders {.,chaincode}/supplychain;
- changing class/type names wherever you find it appropriate
#
# SPDX-License-Identifier: Apache-2.0
#
root = true
[*]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
#
# SPDX-License-Identifier: Apache-2.0
#
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# parcel-bundler cache (https://parceljs.org/)
.cache
# next.js build output
.next
# nuxt.js build output
.nuxt
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless
# Compiled TypeScript files
dist
{
"name": "supplychain-chaincode",
"version": "1.0.0",
"description": "Supplychain chaincode implemented in TypeScript",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
"engines": {
"node": ">=8",
"npm": ">=5"
},
"scripts": {
"lint": "tslint -c tslint.json 'src/**/*.ts'",
"pretest": "npm run lint",
"test": "nyc mocha -r ts-node/register src/**/*.spec.ts",
"start": "fabric-chaincode-node start",
"build": "tsc",
"build:watch": "tsc -w",
"prepublishOnly": "npm run build"
},
"engineStrict": true,
"author": "Hyperledger",
"license": "Apache-2.0",
"dependencies": {
"fabric-contract-api": "unstable",
"fabric-shim": "unstable",
"node-rsa": "^1.0.7"
},
"devDependencies": {
"@types/chai": "^4.1.7",
"@types/mocha": "^5.2.5",
"@types/node": "^10.12.10",
"@types/sinon": "^5.0.7",
"@types/sinon-chai": "^3.2.1",
"@types/node-rsa": "^1.0.0",
"chai": "^4.2.0",
"mocha": "^5.2.0",
"nyc": "^14.1.1",
"sinon": "^7.1.1",
"sinon-chai": "^3.3.0",
"ts-node": "^7.0.1",
"tslint": "^5.11.0",
"typescript": "^3.1.6"
},
"nyc": {
"extension": [
".ts",
".tsx"
],
"exclude": [
"coverage/**",
"dist/**"
],
"reporter": [
"text-summary",
"html"
],
"all": true,
"check-coverage": true,
"statements": 100,
"branches": 100,
"functions": 100,
"lines": 100
}
}
import { Context, Contract } from 'fabric-contract-api';
export abstract class BasicContract extends Contract {
public static mkKey(a:string, b:string): string {
return a + b;
}
protected async query<T>(ctx: Context, whatis: string, id: string): Promise<T> {
const k = BasicContract.mkKey(whatis, id);
return this.queryByFullKey(ctx, k);
}
protected async queryByFullKey<T>(ctx: Context, k: string): Promise<T> {
const valAsBytes = await ctx.stub.getState(k);
if (!valAsBytes || valAsBytes.length === 0) {
throw new Error(`${k} does not exist`);
}
console.log("[queryByFullKey] valAsBytes:");
console.log(valAsBytes);
return JSON.parse(valAsBytes.toString());
}
protected async queryFullRange(ctx: Context, whatis: string): Promise<string> {
const startKey = BasicContract.mkKey(whatis,'0');
const endKey = BasicContract.mkKey(whatis,'999');
const iterator = await ctx.stub.getStateByRange(startKey, endKey);
const allResults = [];
console.log("About to start scanning data for all queries")
while (true) {
const res = await iterator.next();
if (res.value && res.value.value.toString()) {
console.log(res.value.value.toString());
const Key = res.value.key;
let Record;
try {
Record = JSON.parse(res.value.value.toString());
} catch (err) {
console.log(err);
Record = res.value.value.toString();
}
console.log(Record.toString());
allResults.push({ Key, Record});
}
if (res.done) {
console.log('end of data');
await iterator.close();
console.info(allResults);
return JSON.stringify(allResults);
}
}
}
protected async create<T>(ctx: Context, whatis: string, id: string, val: T) {
await ctx.stub.putState(
BasicContract.mkKey(whatis, id),
Buffer.from(JSON.stringify(val))
);
}
protected async replaceByFullKey<T>(ctx: Context, k: string, val: T) {
await ctx.stub.putState(k, Buffer.from(JSON.stringify(val)));
}
protected async replace<T>(ctx: Context, whatis: string, id: string, val: T) {
await this.create(ctx, whatis, id, val);
}
}
\ No newline at end of file
export interface Evaluation {
docType?: string;
val: string;
}
/*
An ItemInfo is a piece of info about an item.
The info we are interested in here are "transfer of ownership" of sort,
e.g. "farmer F0 (src) to shipper S1 (dst)"
*/
export interface ItemInfo {
docType?: string;
item: string; // a tag describing what this item is (e.g. cabbage)
footprint: string; // (encrypted) data on impact of information
src: string;
dst: string;
}
/*
* SPDX-License-Identifier: Apache-2.0
*/
import { SupplyChain } from './supplychain';
export { SupplyChain } from './supplychain';
export const contracts: any[] = [ SupplyChain ];
//import {Bet, Data} from './datatypes';
export {ItemInfo, Evaluation } from './datatypes';
/*
* SPDX-License-Identifier: Apache-2.0
*/
import { Context } from 'fabric-contract-api';
import {BasicContract } from './basiccontract';
import { Evaluation, ItemInfo } from './datatypes';
import * as NodeRSA from 'node-rsa';
/* Crypto functions */
const key = new NodeRSA()
key.importKey({
n: Buffer.from('0086fa9ba066685845fc03833a9699c8baefb53cfbf19052a7f10f1eaa30488cec1ceb752bdff2df9fad6c64b3498956e7dbab4035b4823c99a44cc57088a23783', 'hex'),
e: 65537,
d: Buffer.from('5d2f0dd982596ef781affb1cab73a77c46985c6da2aafc252cea3f4546e80f40c0e247d7d9467750ea1321cc5aa638871b3ed96d19dcc124916b0bcb296f35e1', 'hex'),
p: Buffer.from('00c59419db615e56b9805cc45673a32d278917534804171edcf925ab1df203927f', 'hex'),
q: Buffer.from('00aee3f86b66087abc069b8b1736e38ad6af624f7ea80e70b95f4ff2bf77cd90fd', 'hex'),
dmp1: Buffer.from('008112f5a969fcb56f4e3a4c51a60dcdebec157ee4a7376b843487b53844e8ac85', 'hex'),
dmq1: Buffer.from('1a7370470e0f8a4095df40922a430fe498720e03e1f70d257c3ce34202249d21', 'hex'),
coeff: Buffer.from('00b399675e5e81506b729a777cc03026f0b2119853dfc5eb124610c0ab82999e45', 'hex')
}, 'components');
function decrypt(ciphertext:string)
{
const plaintext = key.decrypt(ciphertext, 'utf8');
console.log(plaintext);
return plaintext;
}
function isItemFinalized(iteminfo):boolean
{
return iteminfo.dst == "D"; // arrived to distributor
}
function computeEvaluation(infos: ItemInfo[]):string
{
// Decryption
// we check it arrived to distribution
// (assuming the earlier steps are present)
let arrivedToDest = false;
let score:number = 0;
// compute score
for (let iteminfo of infos) {
if (isItemFinalized(iteminfo)) {
arrivedToDest = true;
}
const footprintDecrypted = decrypt(iteminfo.footprint);
score += Number(footprintDecrypted);
}
let evalStr = null;
// compute string
if (score <= 3)
evalStr = ":-)";
else if (score <= 8)
evalStr = ":-|";
else
evalStr = ":-(";
if (!arrivedToDest)
return "NA";
else
return evalStr;
}
export class SupplyChain extends BasicContract {
static evalIndexName:string = 'item~eval';
public async initLedger(ctx: Context) {
console.info('============= START : Initialize Ledger ===========');
await this.reset(ctx);
console.info('============= END : Initialize Ledger ===========');
}
protected async insertEvaluation(ctx: Context, tag: string, evaluation: string)
{
let indexKey = await ctx.stub.createCompositeKey(
SupplyChain.evalIndexName, [tag, evaluation]);
console.info(indexKey);
// Save index entry to state. Only the key name is needed, no need to store a duplicate copy of the marble.
// Note - passing a 'nil' value will effectively delete the key from state, therefore we pass null character as value
await ctx.stub.putState(indexKey, Buffer.from('\u0000'));
}
public async reset(ctx:Context) {
await this.insertEvaluation(ctx, "Carrot", ":-)");
}
public async querySimple(ctx: Context, k: string): Promise<string>
{
const valAsBytes = await ctx.stub.getState(k);
if (!valAsBytes || valAsBytes.length === 0) {
throw new Error(`${k} does not exist`);
}
console.log(valAsBytes.toString());
return JSON.parse(valAsBytes.toString());
}
public async addSimple(ctx: Context, k: string, v:string)
{
await ctx.stub.putState(k, Buffer.from(JSON.stringify(v)));
}
public async addItemInfo(ctx: Context, id: string, ii:string)
{
await this.create(ctx, 'iteminfo', id, JSON.parse(ii));
}
// the id of the evaluation is the item id it refers to
public async updateEvaluationOnItem(ctx:Context, tgtTag: string) {
const infosStr = await this.queryInfosByItemTag(ctx, tgtTag);
const infos:ItemInfo[] = JSON.parse(infosStr);
console.log("== Updating evaluation ==");
console.log(infos);
// compute evaluation
const e = computeEvaluation(infos);
await this.insertEvaluation(ctx, tgtTag, e);
}
/* Returns a single evaluations related to item */
public async queryEvaluationByItem(ctx:Context, tgtTag:string): Promise<string> {
return await this.query(ctx, 'evaluation', tgtTag);
}
public async queryAllItemInfos(ctx: Context):Promise<string> {
return await this.queryFullRange(ctx, 'iteminfo');
}
public async queryItemInfoByIdx(ctx: Context, id: string):Promise<string> {
return await this.query(ctx, 'iteminfo', id);
}
public async grabAllInfos(ctx: Context):Promise<ItemInfo []> {
const infosStr = await this.queryAllItemInfos(ctx);
const infosKV = JSON.parse(infosStr);
//const infosKeys = infosKV.map(it => it["Key"]);
const allInfos = infosKV.map(it => JSON.parse(it["Record"]));
console.log(allInfos);
return allInfos;
}
// returns a list [iteminfo ] related to item "item"
public async queryInfosByItemTag(ctx: Context, tgtTag: string):Promise<string> {
const allInfos = await this.grabAllInfos(ctx);
let infosWeCareAbout = [];
for (let x of allInfos) {
if (x.item == tgtTag) {
infosWeCareAbout.push(x);
}
}
return JSON.stringify(infosWeCareAbout);
}
public async queryInfosByItemSrc(ctx: Context, tgtSrc: string):Promise<string> {
const allInfos = await this.grabAllInfos(ctx);
let infosWeCareAbout = [];
for (let x of allInfos) {
if (x.src == tgtSrc) {
infosWeCareAbout.push(x);
}
}
return JSON.stringify(infosWeCareAbout);
}
public async queryInfosByItemDst(ctx: Context, tgtDst: string):Promise<string> {
const allInfos = await this.grabAllInfos(ctx);
let infosWeCareAbout = [];
for (let x of allInfos) {
if (x.dst == tgtDst) {
infosWeCareAbout.push(x);
}
}
return JSON.stringify(infosWeCareAbout);
}
public async queryItemsInDistribution(ctx: Context):Promise<string> {
// Get list of items arrived at Distributor ("D")
const rsltStr = await this.queryInfosByItemDst(ctx, "D");
const rsltItems = JSON.parse(rsltStr).map(x => x.item);
return JSON.stringify(rsltItems);
}
/*
Return all item infos of which the user is the current endpoint
(i.e. the user received it but hasn't shipped it yet)
*/
public async queryPendingItemInfos(ctx: Context, tgtUser: string): Promise<string> {
const receivedItemInfosStr = await this.queryInfosByItemDst(ctx, tgtUser);
const receivedItemInfos:ItemInfo [] = JSON.parse(receivedItemInfosStr);
const shippedItemsInfosStr = await this.queryInfosByItemSrc(ctx, tgtUser);
const shippedItems:string [] = JSON.parse(shippedItemsInfosStr).map(it => it.item);
let pendingItems = [];
for (let rcvdItem of receivedItemInfos) {
if (!shippedItems.includes(rcvdItem.item)) {
pendingItems.push(rcvdItem);
}
}
return JSON.stringify(pendingItems);
}
public async queryItemsWaitingEval(ctx: Context): Promise<string>
{
const allEvalsStr = await this.queryAllEvaluations(ctx);
const allEvaledItems = JSON.parse(allEvalsStr).map(x => x[0]);
const allItemsInDistStr = await this.queryItemsInDistribution(ctx);
let ret = [];
for (let x of JSON.parse(allItemsInDistStr)) {
if (!allEvaledItems.includes(x)) {
// not evaluated yet, then it's pending
ret.push(x);
}
}
return JSON.stringify(ret);
}
public async queryAllEvaluations(ctx: Context): Promise<string> {
let ret = [];
// This will execute a key range query on all keys for evalIndexName
let resultsIterator =
await ctx.stub.getStateByPartialCompositeKey(SupplyChain.evalIndexName, []);
while (true) {
const responseRange = await resultsIterator.next();
if (!responseRange || !responseRange.value || !responseRange.value.key) {
break;
}
console.log(responseRange.value.key);
let objectType;
let attributes;
({
objectType,
attributes
} = await ctx.stub.splitCompositeKey(responseRange.value.key));
ret.push(attributes);
let returnedItem = attributes[0];
let returnedEval = attributes[1];
console.log(`Found item ${returnedItem} with eval ${returnedEval}`);
}
return JSON.stringify(ret);
}
public async queryEvaluation(ctx: Context, tgtItem:string): Promise<string> {
// This will execute a key range query on all keys starting with 'tgtItem'
let resultsIterator =
await ctx.stub.getStateByPartialCompositeKey(SupplyChain.evalIndexName, [tgtItem]);
const responseRange = await resultsIterator.next();
if (!responseRange || !responseRange.value || !responseRange.value.key) {
return;
}
console.log(responseRange.value.key);
let objectType;
let attributes;
({
objectType,
attributes
} = await ctx.stub.splitCompositeKey(responseRange.value.key));
let returnedItem = attributes[0];
let returnedEval = attributes[1];
console.log(`Found item ${returnedItem} with eval ${returnedEval}`);
return returnedEval;
}