From 304e84ec9692a624756da69aae2b96a30a7df7ab Mon Sep 17 00:00:00 2001 From: "abdou.chakhkhar" Date: Fri, 29 Apr 2022 15:25:02 +0000 Subject: [PATCH 1/2] add js cc for the private data Signed-off-by: abdou.chakhkhar --- .../chaincode-javascript/.eslintignore | 5 + .../chaincode-javascript/.eslintrc.js | 39 ++ .../chaincode-javascript/.gitignore | 15 + .../collections_config.json | 35 ++ .../chaincode-javascript/index.js | 12 + .../lib/PrivateAssetTransfer.js | 443 ++++++++++++++++++ .../chaincode-javascript/package.json | 53 +++ .../test/assetTransfer.test.js | 268 +++++++++++ 8 files changed, 870 insertions(+) create mode 100644 asset-transfer-private-data/chaincode-javascript/.eslintignore create mode 100644 asset-transfer-private-data/chaincode-javascript/.eslintrc.js create mode 100644 asset-transfer-private-data/chaincode-javascript/.gitignore create mode 100644 asset-transfer-private-data/chaincode-javascript/collections_config.json create mode 100644 asset-transfer-private-data/chaincode-javascript/index.js create mode 100644 asset-transfer-private-data/chaincode-javascript/lib/PrivateAssetTransfer.js create mode 100644 asset-transfer-private-data/chaincode-javascript/package.json create mode 100644 asset-transfer-private-data/chaincode-javascript/test/assetTransfer.test.js diff --git a/asset-transfer-private-data/chaincode-javascript/.eslintignore b/asset-transfer-private-data/chaincode-javascript/.eslintignore new file mode 100644 index 00000000..15958470 --- /dev/null +++ b/asset-transfer-private-data/chaincode-javascript/.eslintignore @@ -0,0 +1,5 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# + +coverage diff --git a/asset-transfer-private-data/chaincode-javascript/.eslintrc.js b/asset-transfer-private-data/chaincode-javascript/.eslintrc.js new file mode 100644 index 00000000..555c0cf5 --- /dev/null +++ b/asset-transfer-private-data/chaincode-javascript/.eslintrc.js @@ -0,0 +1,39 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +module.exports = { + env: { + node: true, + mocha: true, + es6: 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'], + 'no-constant-condition': ["error", { "checkLoops": false }] + } +}; diff --git a/asset-transfer-private-data/chaincode-javascript/.gitignore b/asset-transfer-private-data/chaincode-javascript/.gitignore new file mode 100644 index 00000000..eeace290 --- /dev/null +++ b/asset-transfer-private-data/chaincode-javascript/.gitignore @@ -0,0 +1,15 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# + +# Coverage directory used by tools like istanbul +coverage + +# Report cache used by istanbul +.nyc_output + +# Dependency directories +node_modules/ +jspm_packages/ + +package-lock.json diff --git a/asset-transfer-private-data/chaincode-javascript/collections_config.json b/asset-transfer-private-data/chaincode-javascript/collections_config.json new file mode 100644 index 00000000..cb3729aa --- /dev/null +++ b/asset-transfer-private-data/chaincode-javascript/collections_config.json @@ -0,0 +1,35 @@ +[ + { + "name": "assetCollection", + "policy": "OR('Org1MSP.member', 'Org2MSP.member')", + "requiredPeerCount": 1, + "maxPeerCount": 1, + "blockToLive":1000000, + "memberOnlyRead": true, + "memberOnlyWrite": true +}, + { + "name": "Org1MSPPrivateCollection", + "policy": "OR('Org1MSP.member')", + "requiredPeerCount": 0, + "maxPeerCount": 1, + "blockToLive":3, + "memberOnlyRead": true, + "memberOnlyWrite": false, + "endorsementPolicy": { + "signaturePolicy": "OR('Org1MSP.member')" + } + }, + { + "name": "Org2MSPPrivateCollection", + "policy": "OR('Org2MSP.member')", + "requiredPeerCount": 0, + "maxPeerCount": 1, + "blockToLive":3, + "memberOnlyRead": true, + "memberOnlyWrite": false, + "endorsementPolicy": { + "signaturePolicy": "OR('Org2MSP.member')" + } + } +] diff --git a/asset-transfer-private-data/chaincode-javascript/index.js b/asset-transfer-private-data/chaincode-javascript/index.js new file mode 100644 index 00000000..04ceb16e --- /dev/null +++ b/asset-transfer-private-data/chaincode-javascript/index.js @@ -0,0 +1,12 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; + +const PrivateAssetTransfer = require('./lib/PrivateAssetTransfer'); + +module.exports.PrivateAssetTransfer = PrivateAssetTransfer; +module.exports.contracts = [PrivateAssetTransfer]; diff --git a/asset-transfer-private-data/chaincode-javascript/lib/PrivateAssetTransfer.js b/asset-transfer-private-data/chaincode-javascript/lib/PrivateAssetTransfer.js new file mode 100644 index 00000000..b2649ce3 --- /dev/null +++ b/asset-transfer-private-data/chaincode-javascript/lib/PrivateAssetTransfer.js @@ -0,0 +1,443 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; + +// Deterministic JSON.stringify() +const stringify = require('json-stringify-deterministic'); +const sortKeysRecursive = require('sort-keys-recursive'); +const { Contract } = require('fabric-contract-api'); + + +const assetCollection = "assetCollection" +const transferAgreementObjectType = "transferAgreement" + +// PrivateData SmartContract +class PrivateAssetTransfer extends Contract { + + // CreateAsset creates a new asset by placing the main asset details in the assetCollection + // that can be read by both organizations. The appraisal value is stored in the owners org specific collection. + async CreateAsset(ctx) { + + // Get the new asset from transient map + const transientMap = await ctx.stub.getTransient(); + + // Asset properties are private, therefore they get passed in transient field, instead of func args + const transientAssetJSON = transientMap.get("asset_properties"); + if (!transientAssetJSON) { + throw new Error('The asset was not found in the transient map input.'); + } + + let assetInput = JSON.parse(transientAssetJSON); + // inputs validation + if (!assetInput.objectType && assetInput.objectType === "") { + throw new Error('objectType field is required, it must be a non-empty string.'); + } + if (!assetInput.assetID && assetInput.assetID === "") { + throw new Error('assetID field is required, it must be a non-empty string'); + } + if (!assetInput.color && assetInput.color === "") { + throw new Error('color field is required, it must be a non-empty string.'); + } + if (!assetInput.size && assetInput.size <= "") { + throw new Error('size field is required, it must be a positive integer.'); + } + if (!assetInput.appraisedValue && assetInput.appraisedValue <= "") { + throw new Error('appraisedValue field is required, it must be a positive integer.'); + } + + // Check if asset already exists + const assetAsBytes = await ctx.stub.getPrivateData(assetCollection, assetInput.assetID); + if (assetAsBytes != '') { + throw new Error(`This asset (${assetInput.assetID}) already exists`); + } + + // Get the ID of submitting client identity + const ClientID = await this.submittingClientIdentity(ctx); + + // Verify that the client is submitting request to peer in their organization + // This is to ensure that a client from another org doesn't attempt to read or + // write private data from this peer. + await this.verifyClientOrgMatchesPeerOrg(ctx); + + const asset = { + objectType: assetInput.objectType, + color: assetInput.color, + assetID: assetInput.assetID, + size: assetInput.size, + owner: ClientID + }; + + // Save asset to private data collection + // Typical logger, logs to stdout/file in the fabric managed docker container, running this chaincode + // Look for container name like dev-peer0.org1.example.com-{chaincodename_version}-xyz + console.log(`CreateAsset Put: collection ${assetCollection}, ID ${assetInput.assetID}, owner ${ClientID}`); + try { + await ctx.stub.putPrivateData(assetCollection, assetInput.assetID, Buffer.from(stringify(sortKeysRecursive(asset)))) + } catch (error) { + throw Error('Failed to put asset into private data collecton.') + } + + // Save asset details to collection visible to owning organization + const assetPrivateDetails = { + ID: assetInput.assetID, + AppraisedValue: assetInput.appraisedValue, + } + + const orgCollection = await this.getCollectionName(ctx); + + // Put asset appraised value into owners org specific private data collection + await ctx.stub.putPrivateData(orgCollection, assetInput.assetID, Buffer.from(stringify(sortKeysRecursive(assetPrivateDetails)))); + + } + + // GetAssetByRange performs a range query based on the start and end keys provided. Range + // queries can be used to read data from private data collections, but can not be used in + // a transaction that also writes to private data. + async GetAssetByRange(ctx, startKey, endKey){ + const response = await ctx.stub.getPrivateDataByRange(assetCollection, startKey, endKey); + const promiseOfIterator = response.iterator.response.results; + + const allResults = []; + for await (const res of promiseOfIterator) { + allResults.push(res.resultBytes.toString()); + } + + return allResults; + } + + // ReadAssetPrivateDetails reads the asset private details in organization specific collection + async ReadAssetPrivateDetails(ctx, collection, assetID){ + const assetDetailsJSON = await ctx.stub.getPrivateData(collection, assetID); + + if(!assetDetailsJSON.toString()){ + throw Error('Failed to read asset details.') + } + return assetDetailsJSON.toString(); + } + + // TransferAsset transfers the asset to the new owner by setting a new owner ID + async TransferAsset (ctx){ + + const transientMap = await ctx.stub.getTransient(); + + // Asset properties are private, therefore they get passed in transient field + const transientTransferJSON = transientMap.get("asset_owner"); + if (!transientTransferJSON) { + throw new Error(`The asset owner not found in the transient map`); + } + const assetTransferInput = { + ID: JSON.parse(transientTransferJSON).assetID, + BuyerMSP: JSON.parse(transientTransferJSON).buyerMSP + } + + if (!assetTransferInput.ID && assetTransferInput.ID === "") { + throw new Error('The assetID field is required, it must be a non-empty string.'); + } + + if (!assetTransferInput.BuyerMSP && assetTransferInput.BuyerMSP === "") { + throw new Error('The buyerMSP field is required, it must be a non-empty string.'); + } + + // Read asset from the private data collection + let asset = await this.ReadAsset(ctx, assetTransferInput.ID); + + if(!asset){ + throw new Error(`${assetTransferInput.ID} does not exist.`); + } + + // Verify that the client is submitting request to peer in their organization + await this.verifyClientOrgMatchesPeerOrg(ctx); + + // Verify transfer details and transfer owner + await this.verifyAgreement(ctx, assetTransferInput.ID, asset.owner, assetTransferInput.BuyerMSP); + + const transferAgreement = await this.ReadTransferAgreement(ctx, assetTransferInput.ID); + + if(!transferAgreement){ + throw new Error(`There has been no agreement related to this asset ${assetTransferInput.ID}.`); + } + + if(!transferAgreement.BuyerID){ + throw new Error(`The BuyerID was not found in TransferAgreement for ${assetTransferInput.ID}.`); + } + + // Transfer asset in private data collection to new owner + console.log(asset); + asset = JSON.parse(asset); + asset.owner = transferAgreement.BuyerID; + + console.log(asset); + + + await ctx.stub.putPrivateData(assetCollection, assetTransferInput.ID, Buffer.from(stringify(sortKeysRecursive(asset)))); + + // Get collection name for this organization + const ownerCollection = await this.getCollectionName(ctx); + + // Delete the asset appraised value from this organization's private data collection + await ctx.stub.deletePrivateData(ownerCollection, assetTransferInput.ID); + + // Delete the transfer agreement from the asset collection + let transferAgreeKey = await ctx.stub.createCompositeKey(transferAgreementObjectType, [assetTransferInput.ID]); + + await ctx.stub.deletePrivateData(assetCollection, transferAgreeKey); + + } + + // ReadAsset reads the information from collection + async ReadAsset(ctx, assetID) { + const assetJSON = await ctx.stub.getPrivateData(assetCollection, assetID); + const asset = assetJSON.toString(); + + //No Asset found, return empty response + if (!asset) { + throw new Error(`${assetID} does not exist in collection ${assetCollection}.`); + } + + return asset; + } + + // AgreeToTransfer is used by the potential buyer of the asset to agree to the + // asset value. The agreed to appraisal value is stored in the buying orgs + // org specifc collection, while the the buyer client ID is stored in the asset collection + // using a composite key + async AgreeToTransfer(ctx){ + + // Get ID of submitting client identity + const ClientID = await this.submittingClientIdentity(ctx); + + // Value is private, therefore it gets passed in transient field + const transientMap = await ctx.stub.getTransient(); + + // Persist the JSON bytes as-is so that there is no risk of nondeterministic marshaling. + const transientAssetJSON = transientMap.get("asset_value"); + + if (!transientAssetJSON) { + throw new Error(`The asset was not found in the transient map input.`); + } + + let valueJSON = JSON.parse(transientAssetJSON); + + // Do some error checking since we get the chance + if (!valueJSON.assetID && valueJSON.assetID === "") { + throw new Error(`assetID field must be a non-empty string`); + } + + if (valueJSON.appraisedValue <= 0) { + throw new Error(`AppraisedValue field must be a non-empty string`); + } + + // Read asset from the private data collection + const asset = await this.ReadAsset(ctx, valueJSON.assetID); + if(!asset){ + throw new Error(`${assetTransferInput.ID} does not exist.`); + } + + // Verify that the client is submitting request to peer in their organization + await this.verifyClientOrgMatchesPeerOrg(ctx); + + // Get collection name for this organization. Needs to be read by a member of the organization. + const orgCollection = await this.getCollectionName(ctx); + + // Put agreed value in the org specifc private data collection + await ctx.stub.putPrivateData(orgCollection, valueJSON.assetID, Buffer.from(stringify(sortKeysRecursive(valueJSON)))) + + // Create agreeement that indicates which identity has agreed to purchase + // In a more realistic transfer scenario, a transfer agreement would be secured to ensure that it cannot + // be overwritten by another channel member. + let transferAgreeKey = await ctx.stub.createCompositeKey(transferAgreementObjectType, [valueJSON.assetID]) + + await ctx.stub.putPrivateData(assetCollection, transferAgreeKey, Buffer.from(stringify(sortKeysRecursive(ClientID)))); + } + + // DeleteTranferAgreement can be used by the buyer to withdraw a proposal from + // the asset collection and from his own collection. + async DeleteTransferAgreement(ctx){ + + const transientMap = await ctx.stub.getTransient(); + if (!transientMap) { + throw new Error(`error getting transient`); + } + + // Asset properties are private, therefore they get passed in transient field + const transientDeleteJSON = transientMap.get("agreement_delete"); + if (!transientDeleteJSON) { + throw new Error(`Asset not found in the transient map input`); + } + + let assetDelete = JSON.parse(transientDeleteJSON); + if (!assetDelete.assetID && assetDelete.assetID === "") { + throw new Error(`assetID field must be a non-empty string`); + } + + // Verify that the client is submitting request to peer in their organization + await this.verifyClientOrgMatchesPeerOrg(ctx); + + // Delete private details of agreement + const orgCollection = await this.getCollectionName(ctx); + + let transferAgreeKey = await ctx.stub.createCompositeKey(transferAgreementObjectType, [assetDelete.assetID]) + + const valAsBytes = await ctx.stub.getPrivateData(assetCollection, transferAgreeKey); + + if (!valAsBytes) { + throw new Error(`Asset's transfer_agreement does not exist.`); + } + + await ctx.stub.deletePrivateData(orgCollection, assetDelete.assetID); + + // Delete transfer agreement record + await ctx.stub.deletePrivateData(assetCollection, transferAgreeKey); + } + + // ReadTransferAgreement gets the buyer's identity from the transfer agreement from collection + async ReadTransferAgreement(ctx, assetID){ + const transferAgreeKey = await ctx.stub.createCompositeKey(transferAgreementObjectType, [assetID]); + const buyerIdentity = await ctx.stub.getPrivateData(assetCollection, transferAgreeKey); + + if(!buyerIdentity.toString()) { + return Error(`TransferAgreement for ${assetID} does not exist.`) + } + + const agreement = { + ID: assetID, + BuyerID: buyerIdentity.toString() + } + return agreement; + } + + // DeleteAsset can be used by the owner of the asset to delete the asset + async DeleteAsset(ctx){ + + const transientMap = await ctx.stub.getTransient(); + if (!transientMap) { + throw new Error(`error getting transient`); + } + // Asset properties are private, therefore they get passed in transient field + const transientDeleteJSON = transientMap.get("asset_delete"); + + if (!transientDeleteJSON) { + throw new Error(`asset not found in the transient map input`); + } + + let assetDelete = JSON.parse(transientDeleteJSON); + if (!assetDelete.assetID && assetDelete.assetID === "") { + throw new Error(`assetID field must be a non-empty string`); + } + + // Verify that the client is submitting request to peer in their organization + await this.verifyClientOrgMatchesPeerOrg(ctx); + + let assetAsBytes = await ctx.stub.getPrivateData(assetCollection, assetDelete.assetID); + if (assetAsBytes == '') { + throw new Error(`The asset not found`); + } + + const ownerCollection = await this.getCollectionName(ctx); + + assetAsBytes = await ctx.stub.getPrivateData(ownerCollection, assetDelete.assetID); + if (assetAsBytes == '') { + throw new Error(`The asset not found in owner s private collection.`); + } + + // delete the asset from state + await ctx.stub.deletePrivateData(assetCollection, assetDelete.assetID) + + // Finally, delete private details of asset + await ctx.stub.deletePrivateData(ownerCollection, assetDelete.assetID) + + } + + // submittingClientIdentity is an internal function to get client identity who submit the transaction. + async submittingClientIdentity(ctx){ + const ClientID = await ctx.clientIdentity.getID(); + if (!ClientID && ClientID === '') { + throw new Error(`Failed to read clientID`); + } + return ClientID; + } + + // verifyClientOrgMatchesPeerOrg is an internal function used verify client org id and matches peer org id. + async verifyClientOrgMatchesPeerOrg(ctx){ + + const ClientMSPID = await ctx.clientIdentity.getMSPID(); + if (!ClientMSPID && ClientMSPID === '') { + throw new Error("Failed getting the client's MSPID."); + } + + const peerMSPID = await ctx.stub.getMspID(); + if (!peerMSPID && peerMSPID === '') { + throw new Error("Failed getting the peer's MSPID."); + } + + if (ClientMSPID !== peerMSPID) { + throw new Error(`Client from org ${ClientMSPID} is not authorized to read or write private data from an org ${peerMSPID} peer.`); + } + } + + // getCollectionName is an internal helper function to get collection of submitting client identity. + async getCollectionName(ctx){ + const ClientMSPID = await ctx.clientIdentity.getMSPID(); + if (!ClientMSPID && ClientMSPID === '') { + throw new Error("Failed getting the client's MSPID."); + } + // Create the collection name + const orgCollection = ClientMSPID + "PrivateCollection"; + return orgCollection; + } + + // verifyAgreement is an internal helper function used by TransferAsset to verify + // that the transfer is being initiated by the owner and that the buyer has agreed + // to the same appraisal value as the owner + async verifyAgreement(ctx, assetID, owner, buyerMSP){ + + // Check 1: verify that the transfer is being initiatied by the owner + + // Get ID of submitting client identity + const ClientID = await this.submittingClientIdentity(ctx); + if (ClientID !== owner) { + return Error("Error: Submitting client identity does not own the asset.") + } + + // Check 2: verify that the buyer has agreed to the appraised value + + // Get collection names + const collectionOwner = await this.getCollectionName(ctx); // get owner collection from caller identity + + const collectionBuyer = buyerMSP + "PrivateCollection"; // get buyers collection + + // Get hash of owners agreed to value + const ownerAppraisedValueHash = await ctx.stub.getPrivateDataHash(collectionOwner, assetID); + + if (!ownerAppraisedValueHash) { + throw Error(`Hash of appraised value for ${assetID} does not exist in collection ${collectionOwner}.`) + } + + // Get hash of buyers agreed to value + const buyerAppraisedValueHash = await ctx.stub.getPrivateDataHash(collectionBuyer, assetID); + + if (!buyerAppraisedValueHash) { + throw Error(`Hash of appraised value for ${assetID} does not exist in collection ${collectionOwner}.`) + } + + console.log("collectionOwner", collectionOwner); + console.log("collectionBuyer", collectionBuyer); + + + console.log("ownerAppraisedValueHash", ownerAppraisedValueHash); + console.log("buyerAppraisedValueHash", buyerAppraisedValueHash); + + // Verify that the two hashes match + if (ownerAppraisedValueHash !== buyerAppraisedValueHash) { + throw new Error(`Hash for the appraised value for owner ${ownerAppraisedValueHash} does not match the value for seller which is ${buyerAppraisedValueHash}.`); + } + + } + +} + +module.exports = PrivateAssetTransfer; diff --git a/asset-transfer-private-data/chaincode-javascript/package.json b/asset-transfer-private-data/chaincode-javascript/package.json new file mode 100644 index 00000000..d4c059d0 --- /dev/null +++ b/asset-transfer-private-data/chaincode-javascript/package.json @@ -0,0 +1,53 @@ +{ + "name": "asset-transfer-basic", + "version": "1.0.0", + "description": "Asset-Transfer-Basic contract implemented in JavaScript", + "main": "index.js", + "engines": { + "node": ">=12", + "npm": ">=5" + }, + "scripts": { + "lint": "eslint *.js */**.js", + "pretest": "npm run lint", + "test": "nyc mocha --recursive", + "start": "fabric-chaincode-node start" + }, + "engineStrict": true, + "author": "Hyperledger", + "license": "Apache-2.0", + "dependencies": { + "fabric-contract-api": "^2.0.0", + "fabric-shim": "^2.0.0", + "json-stringify-deterministic": "^1.0.1", + "node-forge": "^1.3.1", + "openssl-nodejs": "^1.0.5", + "sort-keys-recursive": "^2.1.2" + }, + "devDependencies": { + "chai": "^4.1.2", + "eslint": "^4.19.1", + "mocha": "^8.0.1", + "nyc": "^14.1.1", + "sinon": "^6.0.0", + "sinon-chai": "^3.2.0" + }, + "nyc": { + "exclude": [ + "coverage/**", + "test/**", + "index.js", + ".eslintrc.js" + ], + "reporter": [ + "text-summary", + "html" + ], + "all": true, + "check-coverage": true, + "statements": 100, + "branches": 100, + "functions": 100, + "lines": 100 + } +} diff --git a/asset-transfer-private-data/chaincode-javascript/test/assetTransfer.test.js b/asset-transfer-private-data/chaincode-javascript/test/assetTransfer.test.js new file mode 100644 index 00000000..d50149ad --- /dev/null +++ b/asset-transfer-private-data/chaincode-javascript/test/assetTransfer.test.js @@ -0,0 +1,268 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 +*/ + +'use strict'; +const sinon = require('sinon'); +const chai = require('chai'); +const sinonChai = require('sinon-chai'); +const expect = chai.expect; + +const { Context } = require('fabric-contract-api'); +const { ChaincodeStub } = require('fabric-shim'); + +const AssetTransfer = require('../lib/assetTransfer.js'); + +let assert = sinon.assert; +chai.use(sinonChai); + +describe('Asset Transfer Basic Tests', () => { + let transactionContext, chaincodeStub, asset; + beforeEach(() => { + transactionContext = new Context(); + + chaincodeStub = sinon.createStubInstance(ChaincodeStub); + transactionContext.setChaincodeStub(chaincodeStub); + + chaincodeStub.putState.callsFake((key, value) => { + if (!chaincodeStub.states) { + chaincodeStub.states = {}; + } + chaincodeStub.states[key] = value; + }); + + chaincodeStub.getState.callsFake(async (key) => { + let ret; + if (chaincodeStub.states) { + ret = chaincodeStub.states[key]; + } + return Promise.resolve(ret); + }); + + chaincodeStub.deleteState.callsFake(async (key) => { + if (chaincodeStub.states) { + delete chaincodeStub.states[key]; + } + return Promise.resolve(key); + }); + + chaincodeStub.getStateByRange.callsFake(async () => { + function* internalGetStateByRange() { + if (chaincodeStub.states) { + // Shallow copy + const copied = Object.assign({}, chaincodeStub.states); + + for (let key in copied) { + yield {value: copied[key]}; + } + } + } + + return Promise.resolve(internalGetStateByRange()); + }); + + asset = { + ID: 'asset1', + Color: 'blue', + Size: 5, + Owner: 'Tomoko', + AppraisedValue: 300, + }; + }); + + describe('Test InitLedger', () => { + it('should return error on InitLedger', async () => { + chaincodeStub.putState.rejects('failed inserting key'); + let assetTransfer = new AssetTransfer(); + try { + await assetTransfer.InitLedger(transactionContext); + assert.fail('InitLedger should have failed'); + } catch (err) { + expect(err.name).to.equal('failed inserting key'); + } + }); + + it('should return success on InitLedger', async () => { + let assetTransfer = new AssetTransfer(); + await assetTransfer.InitLedger(transactionContext); + let ret = JSON.parse((await chaincodeStub.getState('asset1')).toString()); + expect(ret).to.eql(Object.assign({docType: 'asset'}, asset)); + }); + }); + + describe('Test CreateAsset', () => { + it('should return error on CreateAsset', async () => { + chaincodeStub.putState.rejects('failed inserting key'); + + let assetTransfer = new AssetTransfer(); + try { + await assetTransfer.CreateAsset(transactionContext, asset.ID, asset.Color, asset.Size, asset.Owner, asset.AppraisedValue); + assert.fail('CreateAsset should have failed'); + } catch(err) { + expect(err.name).to.equal('failed inserting key'); + } + }); + + it('should return success on CreateAsset', async () => { + let assetTransfer = new AssetTransfer(); + + await assetTransfer.CreateAsset(transactionContext, asset.ID, asset.Color, asset.Size, asset.Owner, asset.AppraisedValue); + + let ret = JSON.parse((await chaincodeStub.getState(asset.ID)).toString()); + expect(ret).to.eql(asset); + }); + }); + + describe('Test ReadAsset', () => { + it('should return error on ReadAsset', async () => { + let assetTransfer = new AssetTransfer(); + await assetTransfer.CreateAsset(transactionContext, asset.ID, asset.Color, asset.Size, asset.Owner, asset.AppraisedValue); + + try { + await assetTransfer.ReadAsset(transactionContext, 'asset2'); + assert.fail('ReadAsset should have failed'); + } catch (err) { + expect(err.message).to.equal('The asset asset2 does not exist'); + } + }); + + it('should return success on ReadAsset', async () => { + let assetTransfer = new AssetTransfer(); + await assetTransfer.CreateAsset(transactionContext, asset.ID, asset.Color, asset.Size, asset.Owner, asset.AppraisedValue); + + let ret = JSON.parse(await chaincodeStub.getState(asset.ID)); + expect(ret).to.eql(asset); + }); + }); + + describe('Test UpdateAsset', () => { + it('should return error on UpdateAsset', async () => { + let assetTransfer = new AssetTransfer(); + await assetTransfer.CreateAsset(transactionContext, asset.ID, asset.Color, asset.Size, asset.Owner, asset.AppraisedValue); + + try { + await assetTransfer.UpdateAsset(transactionContext, 'asset2', 'orange', 10, 'Me', 500); + assert.fail('UpdateAsset should have failed'); + } catch (err) { + expect(err.message).to.equal('The asset asset2 does not exist'); + } + }); + + it('should return success on UpdateAsset', async () => { + let assetTransfer = new AssetTransfer(); + await assetTransfer.CreateAsset(transactionContext, asset.ID, asset.Color, asset.Size, asset.Owner, asset.AppraisedValue); + + await assetTransfer.UpdateAsset(transactionContext, 'asset1', 'orange', 10, 'Me', 500); + let ret = JSON.parse(await chaincodeStub.getState(asset.ID)); + let expected = { + ID: 'asset1', + Color: 'orange', + Size: 10, + Owner: 'Me', + AppraisedValue: 500 + }; + expect(ret).to.eql(expected); + }); + }); + + describe('Test DeleteAsset', () => { + it('should return error on DeleteAsset', async () => { + let assetTransfer = new AssetTransfer(); + await assetTransfer.CreateAsset(transactionContext, asset.ID, asset.Color, asset.Size, asset.Owner, asset.AppraisedValue); + + try { + await assetTransfer.DeleteAsset(transactionContext, 'asset2'); + assert.fail('DeleteAsset should have failed'); + } catch (err) { + expect(err.message).to.equal('The asset asset2 does not exist'); + } + }); + + it('should return success on DeleteAsset', async () => { + let assetTransfer = new AssetTransfer(); + await assetTransfer.CreateAsset(transactionContext, asset.ID, asset.Color, asset.Size, asset.Owner, asset.AppraisedValue); + + await assetTransfer.DeleteAsset(transactionContext, asset.ID); + let ret = await chaincodeStub.getState(asset.ID); + expect(ret).to.equal(undefined); + }); + }); + + describe('Test TransferAsset', () => { + it('should return error on TransferAsset', async () => { + let assetTransfer = new AssetTransfer(); + await assetTransfer.CreateAsset(transactionContext, asset.ID, asset.Color, asset.Size, asset.Owner, asset.AppraisedValue); + + try { + await assetTransfer.TransferAsset(transactionContext, 'asset2', 'Me'); + assert.fail('DeleteAsset should have failed'); + } catch (err) { + expect(err.message).to.equal('The asset asset2 does not exist'); + } + }); + + it('should return success on TransferAsset', async () => { + let assetTransfer = new AssetTransfer(); + await assetTransfer.CreateAsset(transactionContext, asset.ID, asset.Color, asset.Size, asset.Owner, asset.AppraisedValue); + + await assetTransfer.TransferAsset(transactionContext, asset.ID, 'Me'); + let ret = JSON.parse((await chaincodeStub.getState(asset.ID)).toString()); + expect(ret).to.eql(Object.assign({}, asset, {Owner: 'Me'})); + }); + }); + + describe('Test GetAllAssets', () => { + it('should return success on GetAllAssets', async () => { + let assetTransfer = new AssetTransfer(); + + await assetTransfer.CreateAsset(transactionContext, 'asset1', 'blue', 5, 'Robert', 100); + await assetTransfer.CreateAsset(transactionContext, 'asset2', 'orange', 10, 'Paul', 200); + await assetTransfer.CreateAsset(transactionContext, 'asset3', 'red', 15, 'Troy', 300); + await assetTransfer.CreateAsset(transactionContext, 'asset4', 'pink', 20, 'Van', 400); + + let ret = await assetTransfer.GetAllAssets(transactionContext); + ret = JSON.parse(ret); + expect(ret.length).to.equal(4); + + let expected = [ + {Record: {ID: 'asset1', Color: 'blue', Size: 5, Owner: 'Robert', AppraisedValue: 100}}, + {Record: {ID: 'asset2', Color: 'orange', Size: 10, Owner: 'Paul', AppraisedValue: 200}}, + {Record: {ID: 'asset3', Color: 'red', Size: 15, Owner: 'Troy', AppraisedValue: 300}}, + {Record: {ID: 'asset4', Color: 'pink', Size: 20, Owner: 'Van', AppraisedValue: 400}} + ]; + + expect(ret).to.eql(expected); + }); + + it('should return success on GetAllAssets for non JSON value', async () => { + let assetTransfer = new AssetTransfer(); + + chaincodeStub.putState.onFirstCall().callsFake((key, value) => { + if (!chaincodeStub.states) { + chaincodeStub.states = {}; + } + chaincodeStub.states[key] = 'non-json-value'; + }); + + await assetTransfer.CreateAsset(transactionContext, 'asset1', 'blue', 5, 'Robert', 100); + await assetTransfer.CreateAsset(transactionContext, 'asset2', 'orange', 10, 'Paul', 200); + await assetTransfer.CreateAsset(transactionContext, 'asset3', 'red', 15, 'Troy', 300); + await assetTransfer.CreateAsset(transactionContext, 'asset4', 'pink', 20, 'Van', 400); + + let ret = await assetTransfer.GetAllAssets(transactionContext); + ret = JSON.parse(ret); + expect(ret.length).to.equal(4); + + let expected = [ + {Record: 'non-json-value'}, + {Record: {ID: 'asset2', Color: 'orange', Size: 10, Owner: 'Paul', AppraisedValue: 200}}, + {Record: {ID: 'asset3', Color: 'red', Size: 15, Owner: 'Troy', AppraisedValue: 300}}, + {Record: {ID: 'asset4', Color: 'pink', Size: 20, Owner: 'Van', AppraisedValue: 400}} + ]; + + expect(ret).to.eql(expected); + }); + }); +}); From d0aaadff22a2f5d8979ab80850510eb3da7388f7 Mon Sep 17 00:00:00 2001 From: "abdou.chakhkhar" Date: Fri, 26 Aug 2022 21:27:35 +0100 Subject: [PATCH 2/2] eslint change --- .../chaincode-javascript/.eslintrc.js | 6 ----- .../lib/PrivateAssetTransfer.js | 25 +++++++++++++++---- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/asset-transfer-private-data/chaincode-javascript/.eslintrc.js b/asset-transfer-private-data/chaincode-javascript/.eslintrc.js index 555c0cf5..02c824f8 100644 --- a/asset-transfer-private-data/chaincode-javascript/.eslintrc.js +++ b/asset-transfer-private-data/chaincode-javascript/.eslintrc.js @@ -12,22 +12,16 @@ module.exports = { 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', diff --git a/asset-transfer-private-data/chaincode-javascript/lib/PrivateAssetTransfer.js b/asset-transfer-private-data/chaincode-javascript/lib/PrivateAssetTransfer.js index b2649ce3..9cc44635 100644 --- a/asset-transfer-private-data/chaincode-javascript/lib/PrivateAssetTransfer.js +++ b/asset-transfer-private-data/chaincode-javascript/lib/PrivateAssetTransfer.js @@ -102,11 +102,26 @@ class PrivateAssetTransfer extends Contract { const promiseOfIterator = response.iterator.response.results; const allResults = []; - for await (const res of promiseOfIterator) { - allResults.push(res.resultBytes.toString()); + let result = await promiseOfIterator.next(); + while (!result.done) { + const strValue = Buffer.from(result.value.value.toString()).toString('utf8'); + let record; + try { + record = JSON.parse(strValue); + } catch (err) { + console.log(err); + record = strValue; + } + allResults.push(record); + result = await promiseOfIterator.next(); } - return allResults; + // for await(const res of promiseOfIterator) { + // allResults.push(res.resultBytes.toString()); + // } + + + return JSON.stringify(allResults); } // ReadAssetPrivateDetails reads the asset private details in organization specific collection @@ -223,7 +238,7 @@ class PrivateAssetTransfer extends Contract { let valueJSON = JSON.parse(transientAssetJSON); // Do some error checking since we get the chance - if (!valueJSON.assetID && valueJSON.assetID === "") { + if (!valueJSON.assetID && valueJSON.assetID === '') { throw new Error(`assetID field must be a non-empty string`); } @@ -412,7 +427,7 @@ class PrivateAssetTransfer extends Contract { // Get hash of owners agreed to value const ownerAppraisedValueHash = await ctx.stub.getPrivateDataHash(collectionOwner, assetID); - + if (!ownerAppraisedValueHash) { throw Error(`Hash of appraised value for ${assetID} does not exist in collection ${collectionOwner}.`) }