diff --git a/commercial-paper/application/application.js b/commercial-paper/application/application.js new file mode 100644 index 0000000000..af11620c85 --- /dev/null +++ b/commercial-paper/application/application.js @@ -0,0 +1,76 @@ +/* +SPDX-License-Identifier: Apache-2.0 +*/ + +/* + * This application has 6 basic steps: + * 1. Select an identity from a wallet + * 2. Connect to network gateway + * 3. Access PaperNet network + * 4. Construct request to issue commercial paper + * 5. Submit transaction + * 6. Process response + */ + +'use strict'; + +// Bring key classes into scope, most importantly Fabric SDK network class +const file = require("fs"); +const yaml = require('js-yaml'); +const { FileSystemWallet, Gateway } = require('fabric-network'); +const { CommercialPaper } = require('./paper.js'); + +// A wallet stores a collection of identities for use +const wallet = new FileSystemWallet('./wallet'); + +// A gateway defines the peers used to access Fabric networks +const gateway = new Gateway(); + +// Main try/catch/finally block +try { + + // Load connection profile; will be used to locate a gateway + connectionProfile = yaml.safeLoad(file.readFileSync('./gateway/connectionProfile.yaml', 'utf8')); + + // Set connection options; use 'admin' identity from application wallet + let connectionOptions = { + identity: 'isabella.the.issuer@magnetocorp.com', + wallet: wallet, + commitTimeout: 100, + strategy: MSPID_SCOPE_ANYFORTX, + commitNotifyStrategy: WAIT_FOR_ALL_CHANNEL_PEER + } + + // Connect to gateway using application specified parameters + await gateway.connect(connectionProfile, connectionOptions); + + console.log('Connected to Fabric gateway.') + + // Get addressability to PaperNet network + const network = await gateway.getNetwork('PaperNet'); + + // Get addressability to commercial paper contract + const contract = await network.getContract('papercontract', 'org.papernet.commercialpaper'); + + console.log('Submit commercial paper issue transaction.') + + // issue commercial paper + const response = await contract.submitTransaction('issue', 'MagnetoCorp', '00001', '2020-05-31', '2020-11-30', '5000000'); + + let paper = CommercialPaper.deserialize(response); + + console.log(`${paper.issuer} commercial paper : ${paper.paperNumber} successfully issued for value ${paper.faceValue}`); + + console.log('Transaction complete.') + +} catch (error) { + + console.log(`Error processing transaction. ${error}`); + +} finally { + + // Disconnect from the gateway + console.log('Disconnect from Fabric gateway.') + gateway.disconnect(); + +} \ No newline at end of file diff --git a/commercial-paper/application/gateway/connectionProfile.yaml b/commercial-paper/application/gateway/connectionProfile.yaml new file mode 100644 index 0000000000..9c6115ccb1 --- /dev/null +++ b/commercial-paper/application/gateway/connectionProfile.yaml @@ -0,0 +1,225 @@ +--- +# +# The network connection profile provides client applications the information about the target +# blockchain network that are necessary for the applications to interact with it. These are all +# knowledge that must be acquired from out-of-band sources. This file provides such a source. +# +name: "global-trade-network" + +# +# Any properties with an "x-" prefix will be treated as application-specific, exactly like how naming +# in HTTP headers or swagger properties work. The SDK will simply ignore these fields and leave +# them for the applications to process. This is a mechanism for different components of an application +# to exchange information that are not part of the standard schema described below. In particular, +# the "x-type" property with the "hlfv1" value example below is used by Hyperledger Composer to +# determine the type of Fabric networks (v0.6 vs. v1.0) it needs to work with. +# +x-type: "hlfv1" + +# +# Describe what the target network is/does. +# +description: "The network to be in if you want to stay in the global trade business" + +# +# Schema version of the content. Used by the SDK to apply the corresponding parsing rules. +# +version: "1.0" + +# +# The client section is SDK-specific. The sample below is for the node.js SDK +# +#client: + # Which organization does this application instance belong to? The value must be the name of an org + # defined under "organizations" + #organization: Org1 + + # Some SDKs support pluggable KV stores, the properties under "credentialStore" + # are implementation specific + #credentialStore: + # [Optional]. Specific to FileKeyValueStore.js or similar implementations in other SDKs. Can be others + # if using an alternative impl. For instance, CouchDBKeyValueStore.js would require an object + # here for properties like url, db name, etc. + #path: "/tmp/hfc-kvs" + + # [Optional]. Specific to the CryptoSuite implementation. Software-based implementations like + # CryptoSuite_ECDSA_AES.js in node SDK requires a key store. PKCS#11 based implementations does + # not. + #cryptoStore: + # Specific to the underlying KeyValueStore that backs the crypto key store. + #path: "/tmp/hfc-cvs" + + # [Optional]. Specific to Composer environment + #wallet: wallet-name + +# +# [Optional]. But most apps would have this section so that channel objects can be constructed +# based on the content below. If an app is creating channels, then it likely will not need this +# section. +# +channels: + # name of the channel + PaperNet: + # Required. list of orderers designated by the application to use for transactions on this + # channel. This list can be a result of access control ("org1" can only access "ordererA"), or + # operational decisions to share loads from applications among the orderers. The values must + # be "names" of orgs defined under "organizations/peers" + orderers: + - orderer.example.com + + # Required. list of peers from participating orgs + peers: + peer1.magnetocorp.com: + # [Optional]. will this peer be sent transaction proposals for endorsement? The peer must + # have the chaincode installed. The app can also use this property to decide which peers + # to send the chaincode install request. Default: true + endorsingPeer: true + + # [Optional]. will this peer be sent query proposals? The peer must have the chaincode + # installed. The app can also use this property to decide which peers to send the + # chaincode install request. Default: true + chaincodeQuery: true + + # [Optional]. will this peer be sent query proposals that do not require chaincodes, like + # queryBlock(), queryTransaction(), etc. Default: true + ledgerQuery: true + + # [Optional]. will this peer be the target of the SDK's listener registration? All peers can + # produce events but the app typically only needs to connect to one to listen to events. + # Default: true + eventSource: true + + peer2.digibank.com: + endorsingPeer: true + chaincodeQuery: false + ledgerQuery: true + eventSource: true + + # [Optional]. what chaincodes are expected to exist on this channel? The application can use + # this information to validate that the target peers are in the expected state by comparing + # this list with the query results of getInstalledChaincodes() and getInstantiatedChaincodes() + chaincodes: + # the format follows the "cannonical name" of chaincodes by fabric code + - example02:v1 + - marbles:1.0 + +# +# list of participating organizations in this network +# +organizations: + Org1: + mspid: magnetocorpMSP + + peers: + - peer1.magnetocorp.com + + # [Optional]. Certificate Authorities issue certificates for identification purposes in a Fabric based + # network. Typically certificates provisioning is done in a separate process outside of the + # runtime network. Fabric-CA is a special certificate authority that provides a REST APIs for + # dynamic certificate management (enroll, revoke, re-enroll). The following section is only for + # Fabric-CA servers. + certificateAuthorities: + - ca-org1 + + # [Optional]. If the application is going to make requests that are reserved to organization + # administrators, including creating/updating channels, installing/instantiating chaincodes, it + # must have access to the admin identity represented by the private key and signing certificate. + # Both properties can be the PEM string or local path to the PEM file. Note that this is mainly for + # convenience in development mode, production systems should not expose sensitive information + # this way. The SDK should allow applications to set the org admin identity via APIs, and only use + # this route as an alternative when it exists. + adminPrivateKey: + path: test/fixtures/channel/crypto-config/peerOrganizations/org1.example.com/users/Admin@org1.example.com/keystore/9022d671ceedbb24af3ea69b5a8136cc64203df6b9920e26f48123fcfcb1d2e9_sk + signedCert: + path: test/fixtures/channel/crypto-config/peerOrganizations/org1.example.com/users/Admin@org1.example.com/signcerts/Admin@org1.example.com-cert.pem + + # the profile will contain public information about organizations other than the one it belongs to. + # These are necessary information to make transaction lifecycles work, including MSP IDs and + # peers with a public URL to send transaction proposals. The file will not contain private + # information reserved for members of the organization, such as admin key and certificate, + # fabric-ca registrar enroll ID and secret, etc. + Org2: + mspid: digibankMSP + peers: + - peer1.digibank.com + certificateAuthorities: + - ca-org2 + adminPrivateKey: + path: test/fixtures/channel/crypto-config/peerOrganizations/org2.example.com/users/Admin@org2.example.com/keystore/5a983ddcbefe52a7f9b8ee5b85a590c3e3a43c4ccd70c7795bec504e7f74848d_sk + signedCert: + path: test/fixtures/channel/crypto-config/peerOrganizations/org2.example.com/users/Admin@org2.example.com/signcerts/Admin@org2.example.com-cert.pem + +# +# List of orderers to send transaction and channel create/update requests to. For the time +# being only one orderer is needed. If more than one is defined, which one get used by the +# SDK is implementation specific. Consult each SDK's documentation for its handling of orderers. +# +orderers: + orderer.example.com: + url: grpcs://localhost:7050 + + # these are standard properties defined by the gRPC library + # they will be passed in as-is to gRPC client constructor + grpcOptions: + ssl-target-name-override: orderer.example.com + + tlsCACerts: + path: test/fixtures/channel/crypto-config/ordererOrganizations/example.com/orderers/orderer.example.com/tlscacerts/example.com-cert.pem + +# +# List of peers to send various requests to, including endorsement, query +# and event listener registration. +# +peers: + peer1.magnetocorp.com: + # this URL is used to send endorsement and query requests + url: grpcs://localhost:7051 + + grpcOptions: + ssl-target-name-override: peer1.magnetocorp.com + request-timeout: 120 + + tlsCACerts: + path: certificates/magnetocorp/magnetocorp.com-cert.pem + + peer1.digibank.com: + url: grpcs://localhost:8051 + grpcOptions: + ssl-target-name-override: peer1.digibank.com + tlsCACerts: + path: certificates/digibank/digibank.com-cert.pem + +# +# Fabric-CA is a special kind of Certificate Authority provided by Hyperledger Fabric which allows +# certificate management to be done via REST APIs. Application may choose to use a standard +# Certificate Authority instead of Fabric-CA, in which case this section would not be specified. +# +certificateAuthorities: + ca-org1: + url: https://localhost:7054 + # the properties specified under this object are passed to the 'http' client verbatim when + # making the request to the Fabric-CA server + httpOptions: + verify: false + tlsCACerts: + path: test/fixtures/channel/crypto-config/peerOrganizations/org1.example.com/ca/org1.example.com-cert.pem + + # Fabric-CA supports dynamic user enrollment via REST APIs. A "root" user, a.k.a registrar, is + # needed to enroll and invoke new users. + registrar: + - enrollId: admin + enrollSecret: adminpw + # [Optional] The optional name of the CA. + caName: ca-org1 + + ca-org2: + url: https://localhost:8054 + httpOptions: + verify: false + tlsCACerts: + path: test/fixtures/channel/crypto-config/peerOrganizations/org2.example.com/ca/org2.example.com-cert.pem + registrar: + - enrollId: admin + enrollSecret: adminpw + # [Optional] The optional name of the CA. + caName: ca-org2 \ No newline at end of file diff --git a/commercial-paper/contract/lib/ledgerutils.js b/commercial-paper/contract/lib/ledgerutils.js new file mode 100644 index 0000000000..b18f60ea2f --- /dev/null +++ b/commercial-paper/contract/lib/ledgerutils.js @@ -0,0 +1,88 @@ +/* +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} JSON 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 {data} data to deserialize into JSON object + * @return {json} json with the data to store + */ + static deserialize(data) { + return JSON.parse(data); + } +} + +/** + * StateList provides a named virtual container for a set of ledger states. + * Each state has a unique key which associates it with the container, rather + * than the container containing a link to the state. This minimizes collisions + * for parallel transactions on different states. + */ +class StateList { + + /** + * Store Fabric context for subsequent API access, and name of list + */ + constructor(ctx, listName) { + this.ctx = ctx; + this.name = listName; + } + + /** + * Add a state to the list. Creates a new state in worldstate with + * appropriate composite key. Note that state defines its own key. + * State object is serialized before writing. + */ + async addState(state) { + let key = this.ctx.stub.createCompositeKey(this.name, [state.getKey()]); + let data = Utils.serialize(state); + await this.ctx.stub.putState(key, data); + } + + /** + * Get a state from the list using supplied keys. Form composite + * keys to retrieve state from world state. State data is deserialized + * into JSON object before being returned. + */ + async getState([keys]) { + let key = this.ctx.stub.createCompositeKey(this.name, [keys]); + let data = await this.ctx.stub.getState(key); + let state = Utils.deserialize(data); + return state; + } + + /** + * Update a state in the list. Puts the new state in world state with + * appropriate composite key. Note that state defines its own key. + * A state is serialized before writing. Logic is very similar to + * addState() but kept separate becuase it is semantically distinct. + */ + async updateState(state) { + let key = this.ctx.stub.createCompositeKey(this.name, [state.getKey()]); + let data = Utils.serialize(state); + await this.ctx.stub.putState(key, data); + } + +} + +module.exports = { + StateList +}; diff --git a/commercial-paper/contract/lib/paper.js b/commercial-paper/contract/lib/paper.js new file mode 100644 index 0000000000..db770cfbe3 --- /dev/null +++ b/commercial-paper/contract/lib/paper.js @@ -0,0 +1,117 @@ +/* +SPDX-License-Identifier: Apache-2.0 +*/ + +'use strict'; + +// Enumerate commercial paper state values +const cpState = { + ISSUED: 1, + TRADING: 2, + REDEEMED: 3 +}; + +/** + * State class. States have a type, unique key, and a lifecycle current state + */ +class State { + constructor(type, [keyParts]) { + this.type = JSON.stringify(type); + this.key = makeKey([keyParts]); + this.currentState = null; + } + + getType() { + return this.type; + } + + static makeKey([keyParts]) { + return keyParts.map(part => JSON.stringify(part)).join(''); + } + + getKey() { + return this.key; + } + +} + +/** + * CommercialPaper class extends State class + * Class will be used by application and smart contract to define a paper + */ +class CommercialPaper extends State { + + constructor(issuer, paperNumber, issueDateTime, maturityDateTime, faceValue) { + super(`org.papernet.commercialpaper`, [issuer, paperNumber]); + + this.issuer = issuer; + this.paperNumber = paperNumber; + this.owner = issuer; + this.issueDateTime = issueDateTime; + this.maturityDateTime = maturityDateTime; + this.faceValue = faceValue; + } + + /** + * Basic getters and setters + */ + 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 + */ + setIssued() { + this.currentState = cpState.ISSUED; + } + + setTrading() { + this.currentState = cpState.TRADING; + } + + setRedeemed() { + this.currentState = cpState.REDEEMED; + } + + isIssued() { + return this.currentState === cpState.ISSUED; + } + + isTrading() { + return this.currentState === cpState.TRADING; + } + + isRedeemed() { + return this.currentState === cpState.REDEEMED; + } + + /** + * Serialize/deserialize commercial paper + **/ + + serialize() { + return Buffer.from(JSON.stringify(this)); + } + + static deserialize(data) { + return Object.create(new CommercialPaper, JSON.parse(data)); + } + +} + +module.exports = { + CommercialPaper, +}; diff --git a/commercial-paper/contract/lib/papercontract.js b/commercial-paper/contract/lib/papercontract.js new file mode 100644 index 0000000000..a4963279bb --- /dev/null +++ b/commercial-paper/contract/lib/papercontract.js @@ -0,0 +1,138 @@ +/* +SPDX-License-Identifier: Apache-2.0 +*/ + +'use strict'; + +// Fabric smart contract classes +const { Contract, Context } = require('fabric-contract-api'); + +// PaperNet specifc classes +const { CommercialPaper } = require('./paper.js'); + +// Utility classes +const { StateList } = require('./ledgerutils.js'); + +/** + * Define custom context for commercial paper by extending Fabric Context class + */ +class CommericalPaperContext extends Context { + + constructor() { + // All papers held ins a list of Fabric states + this.cpList = new StateList(this, 'org.papernet.commercialpaperlist'); + } + +} + +/** + * Define commercial paper smart contract by extending Fabric Contract class + */ +class CommercialPaperContract extends Contract { + + constructor() { + // Unique namespace when multiple contracts per chaincode file + super('org.papernet.commercialpaper'); + } + + // This method is called when a smart contract is instantiated + // Often used to set up the ledger main transactions are called + instantiate() { + + } + + // A custom context provides easy access to the list of commercial papers + createContext() { + return new CommericalPaperContext(); + } + + /** + * 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); + + // Smart contract, rather than paper, moves paper into ISSUED state + cp.setIssued(); + + // Add the paper to the list of all similar commercial papers in the ledger world state + await ctx.cpList.addState(cp); + + return cp.serialize(); + } + + /** + * 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.makeKey([issuer, paperNumber]); + + let cp = await ctx.cpList.getState(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 not already REDEEMED + if (cp.IsTrading()) { + cp.setOwner(newOwner); + } else { + throw new Error('Paper ' + issuer + paperNumber + ' is not trading. Current state = ' + cp.getCurrentState()); + } + + await ctx.cpList.updateState(cp); + return cp.deserialize(); + } + + /** + * 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.makeKey([issuer, paperNumber]); + + let cp = await ctx.cpList.getState(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.updateState(cp); + return cp.serialize(); + } + +} + +module.exports = CommericalPaperContract;