mirror of
https://github.com/hyperledger/fabric-samples.git
synced 2026-06-22 17:45:10 +00:00
Merge "FAB-11723 Developing Apps: Sample pt 1 -- contract"
This commit is contained in:
commit
4e7a174210
10 changed files with 523 additions and 0 deletions
16
commercial-paper/contract/.editorconfig
Executable file
16
commercial-paper/contract/.editorconfig
Executable 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
|
||||||
5
commercial-paper/contract/.eslintignore
Normal file
5
commercial-paper/contract/.eslintignore
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
coverage
|
||||||
37
commercial-paper/contract/.eslintrc.js
Normal file
37
commercial-paper/contract/.eslintrc.js
Normal 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']
|
||||||
|
}
|
||||||
|
};
|
||||||
77
commercial-paper/contract/.npmignore
Normal file
77
commercial-paper/contract/.npmignore
Normal 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
|
||||||
7
commercial-paper/contract/index.js
Normal file
7
commercial-paper/contract/index.js
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
/*
|
||||||
|
SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports.contracts = require('./lib/cpcontract.js');
|
||||||
113
commercial-paper/contract/lib/cpcontract.js
Normal file
113
commercial-paper/contract/lib/cpcontract.js
Normal 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;
|
||||||
148
commercial-paper/contract/lib/cpstate.js
Normal file
148
commercial-paper/contract/lib/cpstate.js
Normal 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
|
||||||
|
};
|
||||||
34
commercial-paper/contract/lib/utils.js
Normal file
34
commercial-paper/contract/lib/utils.js
Normal 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;
|
||||||
45
commercial-paper/contract/package.json
Normal file
45
commercial-paper/contract/package.json
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
41
commercial-paper/contract/test/contract.js
Normal file
41
commercial-paper/contract/test/contract.js
Normal 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);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue