mirror of
https://github.com/hyperledger/fabric-samples.git
synced 2026-06-23 01:55:10 +00:00
add js cc for the private data
Signed-off-by: abdou.chakhkhar <abdelmoula.chakhkhar@uit.ac.ma>
This commit is contained in:
parent
f2fbfa410f
commit
304e84ec96
8 changed files with 870 additions and 0 deletions
|
|
@ -0,0 +1,5 @@
|
|||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
coverage
|
||||
|
|
@ -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 }]
|
||||
}
|
||||
};
|
||||
15
asset-transfer-private-data/chaincode-javascript/.gitignore
vendored
Normal file
15
asset-transfer-private-data/chaincode-javascript/.gitignore
vendored
Normal file
|
|
@ -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
|
||||
|
|
@ -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')"
|
||||
}
|
||||
}
|
||||
]
|
||||
12
asset-transfer-private-data/chaincode-javascript/index.js
Normal file
12
asset-transfer-private-data/chaincode-javascript/index.js
Normal file
|
|
@ -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];
|
||||
|
|
@ -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;
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue