Merge "FAB-11723 Developing Apps: Sample pt 1 -- contract"

This commit is contained in:
Gari Singh 2018-10-06 09:04:20 +00:00 committed by Gerrit Code Review
commit 4e7a174210
10 changed files with 523 additions and 0 deletions

View file

@ -0,0 +1,16 @@
#
# 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

View file

@ -0,0 +1,5 @@
#
# SPDX-License-Identifier: Apache-2.0
#
coverage

View file

@ -0,0 +1,37 @@
/*
SPDX-License-Identifier: Apache-2.0
*/
module.exports = {
env: {
node: true,
mocha: true
},
parserOptions: {
ecmaVersion: 8,
sourceType: 'script'
},
extends: "eslint:recommended",
rules: {
indent: ['error', 4],
'linebreak-style': ['error', 'unix'],
quotes: ['error', 'single'],
semi: ['error', 'always'],
'no-unused-vars': ['error', { args: 'none' }],
'no-console': 'off',
curly: 'error',
eqeqeq: 'error',
'no-throw-literal': 'error',
strict: 'error',
'no-var': 'error',
'dot-notation': 'error',
'no-tabs': 'error',
'no-trailing-spaces': 'error',
'no-use-before-define': 'error',
'no-useless-call': 'error',
'no-with': 'error',
'operator-linebreak': 'error',
yoda: 'error',
'quote-props': ['error', 'as-needed']
}
};

View file

@ -0,0 +1,77 @@
#
# 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

View file

@ -0,0 +1,7 @@
/*
SPDX-License-Identifier: Apache-2.0
*/
'use strict';
module.exports.contracts = require('./lib/cpcontract.js');

View file

@ -0,0 +1,113 @@
/*
SPDX-License-Identifier: Apache-2.0
*/
'use strict';
// Smart contract API brought into scope
const {Contract} = require('fabric-contract-api');
// Commercial paper classes brought into scope
const {CommercialPaper, CommercialPaperList} = require('./cpstate.js');
/**
* Define the commercial paper smart contract extending Fabric Contract class
*/
class CommercialPaperContract extends Contract {
/**
* Each smart contract can have a unique namespace; useful when multiple
* smart contracts per file.
* Use transaction context (ctx) to access list of all commercial papers.
*/
constructor() {
super('org.papernet.commercialpaper');
this.setBeforeFn = (ctx)=>{
ctx.cpList = new CommercialPaperList(ctx, 'COMMERCIALPAPER');
return ctx;
};
}
/**
* Issue commercial paper
* @param {TxContext} ctx the transaction context
* @param {String} issuer commercial paper issuer
* @param {Integer} paperNumber paper number for this issuer
* @param {String} issueDateTime paper issue date
* @param {String} maturityDateTime paper maturity date
* @param {Integer} faceValue face value of paper
*/
async issue(ctx, issuer, paperNumber, issueDateTime, maturityDateTime, faceValue) {
let cp = new CommercialPaper(issuer, paperNumber, issueDateTime, maturityDateTime, faceValue);
// {issuer:"MagnetoCorp", paperNumber:"00001", "May31 2020", "Nov 30 2020", "5M USD"}
await ctx.cpList.addPaper(cp);
}
/**
* Buy commercial paper
* @param {TxContext} ctx the transaction context
* @param {String} issuer commercial paper issuer
* @param {Integer} paperNumber paper number for this issuer
* @param {String} currentOwner current owner of paper
* @param {String} newOwner new owner of paper
* @param {Integer} price price paid for this paper
* @param {String} purchaseDateTime time paper was purchased (i.e. traded)
*/
async buy(ctx, issuer, paperNumber, currentOwner, newOwner, price, purchaseDateTime) {
let cpKey = CommercialPaper.createKey(issuer, paperNumber);
let cp = await ctx.cpList.getPaper(cpKey);
if (cp.getOwner() !== currentOwner) {
throw new Error('Paper '+issuer+paperNumber+' is not owned by '+currentOwner);
}
// First buy moves state from ISSUED to TRADING
if (cp.isIssued()) {
cp.setTrading();
}
// Check paper is TRADING, not REDEEMED
if (cp.IsTrading()) {
cp.setOwner(newOwner);
} else {
throw new Error('Paper '+issuer+paperNumber+' is not trading. Current state = '+cp.getCurrentState());
}
await ctx.cpList.updatePaper(cp);
}
/**
* Redeem commercial paper
* @param {TxContext} ctx the transaction context
* @param {String} issuer commercial paper issuer
* @param {Integer} paperNumber paper number for this issuer
* @param {String} redeemingOwner redeeming owner of paper
* @param {String} redeemDateTime time paper was redeemed
*/
async redeem(ctx, issuer, paperNumber, redeemingOwner, redeemDateTime) {
let cpKey = CommercialPaper.createKey(issuer, paperNumber);
let cp = await ctx.cpList.getPaper(cpKey);
// Check paper is TRADING, not REDEEMED
if (cp.IsRedeemed()) {
throw new Error('Paper '+issuer+paperNumber+' already redeemed');
}
// Verify that the redeemer owns the commercial paper before redeeming it
if (cp.getOwner() === redeemingOwner) {
cp.setOwner(cp.getIssuer());
cp.setRedeemed();
} else {
throw new Error('Redeeming owner does not own paper'+issuer+paperNumber);
}
await ctx.cpList.updatePaper(cp);
}
}
module.exports = CommericalPaperContract;

View file

@ -0,0 +1,148 @@
/*
SPDX-License-Identifier: Apache-2.0
*/
'use strict';
// Helpful utilities class
const Utils = require('./utils.js');
// Enumeration of commercial paper state values
const cpState = {
ISSUED: 1,
TRADING: 2,
REDEEMED: 3
};
/**
* CommercialPaper class defines a commercial paper state
*/
class CommercialPaper {
/**
* Construct a commercial paper. Initial state is issued.
*/
constructor(issuer, paperNumber, issueDateTime, maturityDateTime, faceValue) {
this.issuer = issuer;
this.paperNumber = paperNumber;
this.owner = issuer;
this.issueDateTime = issueDateTime;
this.maturityDateTime = maturityDateTime;
this.faceValue = faceValue;
this.currentState = cpState.ISSUED;
this.key = CommercialPaper.createKey(issuer, paperNumber);
}
/**
* The commercial paper is uniquely identified by its key.
* The key is a simple composite of issuer and paper number as strings.
*/
static createKey(issuer, paperNumber) {
return JSON.stringify(issuer) + JSON.stringify(paperNumber);
}
/**
* Basic getters and setters
*/
getKey() {
return this.key;
}
getIssuer() {
return this.issuer;
}
setIssuer(newIssuer) {
this.issuer = newIssuer;
}
getOwner() {
return this.owner;
}
setOwner(newOwner) {
this.owner = newOwner;
}
/**
* Useful methods to encapsulate commercial paper states
*/
setTrading() {
this.currentState = cpState.TRADING;
}
setRedeemed() {
this.currentState = cpState.REDEEMED;
}
isTrading() {
return this.currentState === cpState.TRADING;
}
isRedeemed() {
return this.currentState === cpState.REDEEMED;
}
}
/**
* CommercialPaperList provides a virtual container to access all
* commercial papers. Each paper has unique key which associates it
* with the container, rather than the container containing a link to
* the paper. This is important in Fabric becuase it minimizes
* collisions for parallel transactions on different papers.
*/
class CommercialPaperList {
/**
* For this sample, it is sufficient to create a commercial paper list
* using a fixed container prefix. The transaction context is saved to
* access Fabric APIs when required.
*/
constructor(ctx, prefix) {
this.api = ctx.stub;
this.prefix = prefix;
}
/**
* Add a paper to the list. Creates a new state in worldstate with
* appropriate composite key. Note that paper defines its own key.
* Paper object is serialized before writing.
*/
async addPaper(cp) {
let key = this.api.createCompositeKey(this.prefix, [cp.getKey()]);
let data = Utils.serialize(cp);
await this.api.putState(key, data);
}
/**
* Get a paper from the list using issuer and paper number. Forms composite
* keys to retrieve data from world state. State data is deserialized
* into paper object before being returned.
*/
async getPaper(key) {
let key = this.api.createCompositeKey(this.prefix, [key]);
let data = await this.api.getState(key);
let cp = Utils.deserialize(data);
return cp;
}
/**
* Update a paper in the list. Puts the new state in world state with
* appropriate composite key. Note that paper defines its own key.
* Paper object is serialized before writing. Logic is very similar to
* addPaper() but kept separate becuase it is semantically distinct, and
* may change.
*/
async updatePaper(cp) {
let key = this.api.createCompositeKey(this.prefix, [cp.getKey()]);
let data = Utils.serialize(cp);
await this.api.putState(key, data);
}
}
module.exports = {
CommercialPaper,
CommercialPaperList
};

View file

@ -0,0 +1,34 @@
/*
SPDX-License-Identifier: Apache-2.0
*/
'use strict';
/**
* Utility class for data, object mapulation, e.g. serialization
*/
class Utils {
/**
* Convert object to buffer containing JSON data serialization
* Typically used before putState() ledger API
* @param {Object} object object to serialize
* @return {buffer} buffer with the data to store
*/
static serialize(object){
return Buffer.from(JSON.stringify(object));
}
/**
* Deserialize object, i.e. Covert serialized data to JSON object
* Typically used after getState() ledger API
* @param {Object} data object to deserialize
* @return {json} json with the data to store
*/
static deserialize(data){
return JSON.parse(data);
}
}
module.exports = Utils;

View file

@ -0,0 +1,45 @@
{
"name": "smart-contract",
"version": "0.0.1",
"description": "Smart Contract",
"engines": {
"node": ">=8",
"npm": ">=5"
},
"scripts": {
"lint": "eslint .",
"pretest": "npm run lint",
"test": "nyc mocha --recursive",
"start": "startChaincode"
},
"engineStrict": true,
"author": "Anthony ODowd",
"license": "Apache-2.0",
"dependencies": {
"fabric-shim": "unstable",
"fabric-contract-api": "unstable"
},
"devDependencies": {
"chai": "^4.1.2",
"eslint": "^4.19.1",
"mocha": "^5.2.0",
"nyc": "^12.0.2",
"sinon": "^6.0.0"
},
"nyc": {
"exclude": [
"coverage/**",
"test/**"
],
"reporter": [
"text-summary",
"html"
],
"all": true,
"check-coverage": true,
"statements": 100,
"branches": 100,
"functions": 100,
"lines": 100
}
}

View file

@ -0,0 +1,41 @@
/*
SPDX-License-Identifier: Apache-2.0
*/
'use strict';
const Chaincode = require('../lib/chaincode');
const { Stub } = require('fabric-shim');
require('chai').should();
const sinon = require('sinon');
describe('Chaincode', () => {
describe('#Init', () => {
it('should work', async () => {
const cc = new Chaincode();
const stub = sinon.createStubInstance(Stub);
stub.getFunctionAndParameters.returns({ fcn: 'initFunc', params: [] });
const res = await cc.Init(stub);
res.status.should.equal(Stub.RESPONSE_CODE.OK);
});
});
describe('#Invoke', async () => {
it('should work', async () => {
const cc = new Chaincode();
const stub = sinon.createStubInstance(Stub);
stub.getFunctionAndParameters.returns({ fcn: 'initFunc', params: [] });
let res = await cc.Init(stub);
res.status.should.equal(Stub.RESPONSE_CODE.OK);
stub.getFunctionAndParameters.returns({ fcn: 'invokeFunc', params: [] });
res = await cc.Invoke(stub);
res.status.should.equal(Stub.RESPONSE_CODE.OK);
});
});
});