diff --git a/commercial-paper/contract/.editorconfig b/commercial-paper/contract/.editorconfig new file mode 100755 index 0000000000..75a13be205 --- /dev/null +++ b/commercial-paper/contract/.editorconfig @@ -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 diff --git a/commercial-paper/contract/.eslintignore b/commercial-paper/contract/.eslintignore new file mode 100644 index 0000000000..1595847010 --- /dev/null +++ b/commercial-paper/contract/.eslintignore @@ -0,0 +1,5 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# + +coverage diff --git a/commercial-paper/contract/.eslintrc.js b/commercial-paper/contract/.eslintrc.js new file mode 100644 index 0000000000..6772c660df --- /dev/null +++ b/commercial-paper/contract/.eslintrc.js @@ -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'] + } +}; diff --git a/commercial-paper/contract/.npmignore b/commercial-paper/contract/.npmignore new file mode 100644 index 0000000000..a00ca94150 --- /dev/null +++ b/commercial-paper/contract/.npmignore @@ -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 diff --git a/commercial-paper/contract/index.js b/commercial-paper/contract/index.js new file mode 100644 index 0000000000..ddc4f2c6fb --- /dev/null +++ b/commercial-paper/contract/index.js @@ -0,0 +1,7 @@ +/* +SPDX-License-Identifier: Apache-2.0 +*/ + +'use strict'; + +module.exports.contracts = require('./lib/cpcontract.js'); diff --git a/commercial-paper/contract/lib/cpcontract.js b/commercial-paper/contract/lib/cpcontract.js new file mode 100644 index 0000000000..ed793fd16f --- /dev/null +++ b/commercial-paper/contract/lib/cpcontract.js @@ -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; diff --git a/commercial-paper/contract/lib/cpstate.js b/commercial-paper/contract/lib/cpstate.js new file mode 100644 index 0000000000..ad5222cde2 --- /dev/null +++ b/commercial-paper/contract/lib/cpstate.js @@ -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 +}; diff --git a/commercial-paper/contract/lib/utils.js b/commercial-paper/contract/lib/utils.js new file mode 100644 index 0000000000..2a2c08ad70 --- /dev/null +++ b/commercial-paper/contract/lib/utils.js @@ -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; diff --git a/commercial-paper/contract/package.json b/commercial-paper/contract/package.json new file mode 100644 index 0000000000..5741509f1c --- /dev/null +++ b/commercial-paper/contract/package.json @@ -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 + } +} diff --git a/commercial-paper/contract/test/contract.js b/commercial-paper/contract/test/contract.js new file mode 100644 index 0000000000..e0aafd5e97 --- /dev/null +++ b/commercial-paper/contract/test/contract.js @@ -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); + }); + + }); + +});