fabric-samples/asset-transfer-private-data/chaincode-javascript/lib/PrivateAssetTransfer.js
abdou.chakhkhar 304e84ec96 add js cc for the private data
Signed-off-by: abdou.chakhkhar <abdelmoula.chakhkhar@uit.ac.ma>
2022-05-01 17:07:34 +00:00

443 lines
18 KiB
JavaScript

/*
* 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;