mirror of
https://github.com/hyperledger/fabric-samples.git
synced 2026-06-17 15:35:09 +00:00
* Adding golang application for asset-transfer-basic sample. (#211) Signed-off-by: Chongxin Luo <Chongxin.Luo@ibm.com> Improved private data Go Chaincode in idiomatic go. Adding go chaincode unit tests Signed-off-by: Sijo Cherian <sijo@ibm.com> * Added unit tests for query-asset chaincode functions Signed-off-by: Sijo Cherian <sijo@ibm.com> * Improved README Signed-off-by: Sijo Cherian <sijo@ibm.com> * Added unit tests for query-asset chaincode functions Signed-off-by: Sijo Cherian <sijo@ibm.com> * Fixed json.Marsal usage per review comments, Improved DeleteAsset validation Added owner collection check for DeleteAsset chaincode JS app now demos a new expected error on DeleteAsset by a non-owner org Signed-off-by: Sijo Cherian <sijo@ibm.com> Co-authored-by: Dereck <Chongxin.Luo@ibm.com> Co-authored-by: Sijo Cherian <sijo@ibm.com>
283 lines
15 KiB
JavaScript
283 lines
15 KiB
JavaScript
/*
|
|
* Copyright IBM Corp. All Rights Reserved.
|
|
*
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const { Gateway, Wallets } = require('fabric-network');
|
|
const FabricCAServices = require('fabric-ca-client');
|
|
const path = require('path');
|
|
const { buildCAClient, registerAndEnrollUser, enrollAdmin } = require('../../test-application/javascript/CAUtil.js');
|
|
const { buildCCPOrg1, buildCCPOrg2, buildWallet } = require('../../test-application/javascript/AppUtil.js');
|
|
|
|
const myChannel = 'mychannel';
|
|
const myChaincodeName = 'private';
|
|
|
|
const memberAssetCollectionName = 'assetCollection';
|
|
const org1PrivateCollectionName = 'Org1MSPPrivateCollection';
|
|
const org2PrivateCollectionName = 'Org2MSPPrivateCollection';
|
|
const mspOrg1 = 'Org1MSP';
|
|
const mspOrg2 = 'Org2MSP';
|
|
const Org1UserId = 'appUser1';
|
|
const Org2UserId = 'appUser2';
|
|
|
|
function prettyJSONString(inputString) {
|
|
if (inputString) {
|
|
return JSON.stringify(JSON.parse(inputString), null, 2);
|
|
}
|
|
else {
|
|
return inputString;
|
|
}
|
|
}
|
|
|
|
async function initContractFromOrg1Identity() {
|
|
console.log('\n--> Fabric client user & Gateway init: Using Org1 identity to Org1 Peer');
|
|
// build an in memory object with the network configuration (also known as a connection profile)
|
|
const ccpOrg1 = buildCCPOrg1();
|
|
|
|
// build an instance of the fabric ca services client based on
|
|
// the information in the network configuration
|
|
const caOrg1Client = buildCAClient(FabricCAServices, ccpOrg1, 'ca.org1.example.com');
|
|
|
|
// setup the wallet to cache the credentials of the application user, on the app server locally
|
|
const walletPathOrg1 = path.join(__dirname, 'wallet/org1');
|
|
const walletOrg1 = await buildWallet(Wallets, walletPathOrg1);
|
|
|
|
// in a real application this would be done on an administrative flow, and only once
|
|
// stores admin identity in local wallet, if needed
|
|
await enrollAdmin(caOrg1Client, walletOrg1, mspOrg1);
|
|
// register & enroll application user with CA, which is used as client identify to make chaincode calls
|
|
// and stores app user identity in local wallet
|
|
// In a real application this would be done only when a new user was required to be added
|
|
// and would be part of an administrative flow
|
|
await registerAndEnrollUser(caOrg1Client, walletOrg1, mspOrg1, Org1UserId, 'org1.department1');
|
|
|
|
try {
|
|
// Create a new gateway for connecting to Org's peer node.
|
|
const gatewayOrg1 = new Gateway();
|
|
//connect using Discovery enabled
|
|
await gatewayOrg1.connect(ccpOrg1,
|
|
{ wallet: walletOrg1, identity: Org1UserId, discovery: { enabled: true, asLocalhost: true } });
|
|
|
|
return gatewayOrg1;
|
|
} catch (error) {
|
|
console.error(`Error in connecting to gateway: ${error}`);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
async function initContractFromOrg2Identity() {
|
|
console.log('\n--> Fabric client user & Gateway init: Using Org2 identity to Org2 Peer');
|
|
const ccpOrg2 = buildCCPOrg2();
|
|
const caOrg2Client = buildCAClient(FabricCAServices, ccpOrg2, 'ca.org2.example.com');
|
|
|
|
const walletPathOrg2 = path.join(__dirname, 'wallet/org2');
|
|
const walletOrg2 = await buildWallet(Wallets, walletPathOrg2);
|
|
|
|
await enrollAdmin(caOrg2Client, walletOrg2, mspOrg2);
|
|
await registerAndEnrollUser(caOrg2Client, walletOrg2, mspOrg2, Org2UserId, 'org2.department1');
|
|
|
|
try {
|
|
// Create a new gateway for connecting to Org's peer node.
|
|
const gatewayOrg2 = new Gateway();
|
|
await gatewayOrg2.connect(ccpOrg2,
|
|
{ wallet: walletOrg2, identity: Org2UserId, discovery: { enabled: true, asLocalhost: true } });
|
|
|
|
return gatewayOrg2;
|
|
} catch (error) {
|
|
console.error(`Error in connecting to gateway: ${error}`);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
// Main workflow : usecase details at asset-transfer-private-data/chaincode-go/README.md
|
|
// This app uses fabric-samples/test-network based setup and the companion chaincode
|
|
// For this usecase illustration, we will use both Org1 & Org2 client identity from this same app
|
|
// In real world the Org1 & Org2 identity will be used in different apps to achieve asset transfer.
|
|
async function main() {
|
|
try {
|
|
|
|
/** ******* Fabric client init: Using Org1 identity to Org1 Peer ********** */
|
|
const gatewayOrg1 = await initContractFromOrg1Identity();
|
|
const networkOrg1 = await gatewayOrg1.getNetwork(myChannel);
|
|
const contractOrg1 = networkOrg1.getContract(myChaincodeName);
|
|
// Since this sample chaincode uses, Private Data Collection level endorsement policy, addDiscoveryInterest
|
|
// scopes the discovery service further to use the endorsement policies of collections, if any
|
|
contractOrg1.addDiscoveryInterest({ name: myChaincodeName, collectionNames: [memberAssetCollectionName, org1PrivateCollectionName] });
|
|
|
|
/** ~~~~~~~ Fabric client init: Using Org2 identity to Org2 Peer ~~~~~~~ */
|
|
const gatewayOrg2 = await initContractFromOrg2Identity();
|
|
const networkOrg2 = await gatewayOrg2.getNetwork(myChannel);
|
|
const contractOrg2 = networkOrg2.getContract(myChaincodeName);
|
|
contractOrg2.addDiscoveryInterest({ name: myChaincodeName, collectionNames: [memberAssetCollectionName, org2PrivateCollectionName] });
|
|
try {
|
|
// Sample transactions are listed below
|
|
// Add few sample Assets & transfers one of the asset from Org1 to Org2 as the new owner
|
|
let assetID1 = 'asset1';
|
|
let assetID2 = 'asset2';
|
|
const assetType = 'ValuableAsset';
|
|
let result;
|
|
let asset1Data = { objectType: assetType, assetID: assetID1, color: 'green', size: 20, appraisedValue: 100 };
|
|
let asset2Data = { objectType: assetType, assetID: assetID2, color: 'blue', size: 35, appraisedValue: 727 };
|
|
|
|
console.log('\n**************** As Org1 Client ****************');
|
|
console.log('Adding Assets to work with:\n--> Submit Transaction: CreateAsset ' + assetID1);
|
|
let statefulTxn = contractOrg1.createTransaction('CreateAsset');
|
|
//if you need to customize endorsement to specific set of Orgs, use setEndorsingOrganizations
|
|
//statefulTxn.setEndorsingOrganizations(mspOrg1);
|
|
let tmapData = Buffer.from(JSON.stringify(asset1Data));
|
|
statefulTxn.setTransient({
|
|
asset_properties: tmapData
|
|
});
|
|
result = await statefulTxn.submit();
|
|
|
|
//Add asset2
|
|
console.log('\n--> Submit Transaction: CreateAsset ' + assetID2);
|
|
statefulTxn = contractOrg1.createTransaction('CreateAsset');
|
|
tmapData = Buffer.from(JSON.stringify(asset2Data));
|
|
statefulTxn.setTransient({
|
|
asset_properties: tmapData
|
|
});
|
|
result = await statefulTxn.submit();
|
|
|
|
|
|
console.log('\n--> Evaluate Transaction: GetAssetByRange asset0-asset9');
|
|
// GetAssetByRange returns assets on the ledger with ID in the range of startKey (inclusive) and endKey (exclusive)
|
|
result = await contractOrg1.evaluateTransaction('GetAssetByRange', 'asset0', 'asset9');
|
|
console.log(' result: ' + prettyJSONString(result.toString()));
|
|
|
|
console.log('\n--> Evaluate Transaction: ReadAssetPrivateDetails from ' + org1PrivateCollectionName);
|
|
// ReadAssetPrivateDetails reads data from Org's private collection. Args: collectionName, assetID
|
|
result = await contractOrg1.evaluateTransaction('ReadAssetPrivateDetails', org1PrivateCollectionName, assetID1);
|
|
console.log(' result: ' + prettyJSONString(result.toString()));
|
|
|
|
// Attempt Transfer the asset to Org2 , without Org2 adding AgreeToTransfer //
|
|
// Transaction should return an error: "failed transfer verification ..."
|
|
let buyerDetails = { assetID: assetID1, buyerMSP: mspOrg2 };
|
|
try {
|
|
console.log('\n--> Attempt Submit Transaction: TransferAsset ' + assetID1);
|
|
statefulTxn = contractOrg1.createTransaction('TransferAsset');
|
|
tmapData = Buffer.from(JSON.stringify(buyerDetails));
|
|
statefulTxn.setTransient({
|
|
asset_owner: tmapData
|
|
});
|
|
result = await statefulTxn.submit();
|
|
console.log('******** FAILED: above operation expected to return an error');
|
|
} catch (error) {
|
|
console.log(` Successfully caught the error: \n ${error}`);
|
|
}
|
|
console.log('\n~~~~~~~~~~~~~~~~ As Org2 Client ~~~~~~~~~~~~~~~~');
|
|
console.log('\n--> Evaluate Transaction: ReadAsset ' + assetID1);
|
|
result = await contractOrg2.evaluateTransaction('ReadAsset', assetID1);
|
|
console.log(' result: ' + prettyJSONString(result.toString()));
|
|
let assetOwner = JSON.parse(result.toString()).owner;
|
|
console.log(' Asset owner: ' + Buffer.from(assetOwner, 'base64').toString());
|
|
|
|
// Org2 cannot ReadAssetPrivateDetails from Org1's private collection due to Collection policy
|
|
// Will fail: await contractOrg2.evaluateTransaction('ReadAssetPrivateDetails', org1PrivateCollectionName, assetID1);
|
|
|
|
// Buyer from Org2 agrees to buy the asset assetID1 //
|
|
// To purchase the asset, the buyer needs to agree to the same value as the asset owner
|
|
let dataForAgreement = { assetID: assetID1, appraisedValue: 100 };
|
|
console.log('\n--> Submit Transaction: AgreeToTransfer payload ' + JSON.stringify(dataForAgreement));
|
|
statefulTxn = contractOrg2.createTransaction('AgreeToTransfer');
|
|
tmapData = Buffer.from(JSON.stringify(dataForAgreement));
|
|
statefulTxn.setTransient({
|
|
asset_value: tmapData
|
|
});
|
|
result = await statefulTxn.submit();
|
|
|
|
//Buyer can withdraw the Agreement, using DeleteTranferAgreement
|
|
/*statefulTxn = contractOrg2.createTransaction('DeleteTranferAgreement');
|
|
statefulTxn.setEndorsingOrganizations(mspOrg2);
|
|
let dataForDeleteAgreement = { assetID: assetID1 };
|
|
tmapData = Buffer.from(JSON.stringify(dataForDeleteAgreement));
|
|
statefulTxn.setTransient({
|
|
agreement_delete: tmapData
|
|
});
|
|
result = await statefulTxn.submit();*/
|
|
|
|
console.log('\n**************** As Org1 Client ****************');
|
|
// All members can send txn ReadTransferAgreement, set by Org2 above
|
|
console.log('\n--> Evaluate Transaction: ReadTransferAgreement ' + assetID1);
|
|
result = await contractOrg1.evaluateTransaction('ReadTransferAgreement', assetID1);
|
|
console.log(' result: ' + prettyJSONString(result.toString()));
|
|
|
|
// Transfer the asset to Org2 //
|
|
// To transfer the asset, the owner needs to pass the MSP ID of new asset owner, and initiate the transfer
|
|
console.log('\n--> Submit Transaction: TransferAsset ' + assetID1);
|
|
|
|
statefulTxn = contractOrg1.createTransaction('TransferAsset');
|
|
tmapData = Buffer.from(JSON.stringify(buyerDetails));
|
|
statefulTxn.setTransient({
|
|
asset_owner: tmapData
|
|
});
|
|
result = await statefulTxn.submit();
|
|
|
|
//Again ReadAsset : results will show that the buyer identity now owns the asset:
|
|
console.log('\n--> Evaluate Transaction: ReadAsset ' + assetID1);
|
|
result = await contractOrg1.evaluateTransaction('ReadAsset', assetID1);
|
|
console.log(' result: ' + prettyJSONString(result.toString()));
|
|
assetOwner = JSON.parse(result.toString()).owner;
|
|
console.log(' Asset owner: ' + Buffer.from(assetOwner, 'base64').toString());
|
|
|
|
//Confirm that transfer removed the private details from the Org1 collection:
|
|
console.log('\n--> Evaluate Transaction: ReadAssetPrivateDetails');
|
|
// ReadAssetPrivateDetails reads data from Org's private collection: Should return empty
|
|
result = await contractOrg1.evaluateTransaction('ReadAssetPrivateDetails', org1PrivateCollectionName, assetID1);
|
|
console.log(' result: ' + prettyJSONString(result.toString()));
|
|
|
|
console.log('\n--> Evaluate Transaction: ReadAsset ' + assetID2);
|
|
result = await contractOrg1.evaluateTransaction('ReadAsset', assetID2);
|
|
console.log(' result: ' + prettyJSONString(result.toString()));
|
|
|
|
console.log('\n********* Demo deleting asset **************');
|
|
let dataForDelete = { assetID: assetID2 };
|
|
try {
|
|
//Non-owner Org2 should not be able to DeleteAsset. Expect an error from DeleteAsset
|
|
console.log('--> Attempt Transaction: as Org2 DeleteAsset ' + assetID2);
|
|
statefulTxn = contractOrg2.createTransaction('DeleteAsset');
|
|
tmapData = Buffer.from(JSON.stringify(dataForDelete));
|
|
statefulTxn.setTransient({
|
|
asset_delete: tmapData
|
|
});
|
|
result = await statefulTxn.submit();
|
|
console.log('******** FAILED : expected to return an error');
|
|
} catch (error) {
|
|
console.log(` Successfully caught the error: \n ${error}`);
|
|
}
|
|
// Delete Asset2 as Org1
|
|
console.log('--> Submit Transaction: as Org1 DeleteAsset ' + assetID2);
|
|
statefulTxn = contractOrg1.createTransaction('DeleteAsset');
|
|
tmapData = Buffer.from(JSON.stringify(dataForDelete));
|
|
statefulTxn.setTransient({
|
|
asset_delete: tmapData
|
|
});
|
|
result = await statefulTxn.submit();
|
|
|
|
console.log('\n--> Evaluate Transaction: ReadAsset ' + assetID2);
|
|
result = await contractOrg1.evaluateTransaction('ReadAsset', assetID2);
|
|
console.log(' result: ' + prettyJSONString(result.toString()));
|
|
|
|
console.log('\n~~~~~~~~~~~~~~~~ As Org2 Client ~~~~~~~~~~~~~~~~');
|
|
// Org2 can ReadAssetPrivateDetails: Org2 is owner, and private details exist in new owner's Collection
|
|
console.log('\n--> Evaluate Transaction as Org2: ReadAssetPrivateDetails ' + assetID1 + ' from ' + org2PrivateCollectionName);
|
|
result = await contractOrg2.evaluateTransaction('ReadAssetPrivateDetails', org2PrivateCollectionName, assetID1);
|
|
console.log(' result: ' + prettyJSONString(result.toString()));
|
|
} finally {
|
|
// Disconnect from the gateway peer when all work for this client identity is complete
|
|
gatewayOrg1.disconnect();
|
|
gatewayOrg2.disconnect();
|
|
}
|
|
} catch (error) {
|
|
console.error(`Error in transaction: ${error}`);
|
|
if (error.stack) {
|
|
console.error(error.stack);
|
|
}
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
main();
|