mirror of
https://github.com/hyperledger/fabric-samples.git
synced 2026-06-23 10:05:10 +00:00
458 lines
18 KiB
JavaScript
458 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 = [];
|
|
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();
|
|
}
|
|
|
|
// 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
|
|
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;
|