mirror of
https://github.com/hyperledger/fabric-samples.git
synced 2026-06-17 15:35:09 +00:00
* Adding JS App workflow, Bugfixes in Go Chaincode Initial impl JS App workflow, Added 2 asset sample, with transfer & delete Bugfixes: Bug fix in ReadTransferAgreement Improved logging in Go Chaincode, Improved Chaincode error handling, checks for execution on orgs peer, bug fix, lint issues use addDiscoveryInterest to scope policy, instead of setEndorsingOrganizations Signed-off-by: Sijo Cherian <sijo@ibm.com> * added js app to CI pipeline , and linting matrix Signed-off-by: Sijo Cherian <sijo@ibm.com> Co-authored-by: Sijo Cherian <sijo@ibm.com>
273 lines
No EOL
14 KiB
JavaScript
273 lines
No EOL
14 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 fs = require('fs');
|
|
const caUtils = require('./caUtils');
|
|
|
|
const myChannel = 'mychannel';
|
|
const myChaincodeName = 'private';
|
|
|
|
const memberAssetCollectionName = 'assetCollection';
|
|
const org1PrivateCollectionName = 'Org1MSPPrivateCollection';
|
|
const org2PrivateCollectionName = 'Org2MSPPrivateCollection';
|
|
const mspOrg2 = 'Org2MSP';
|
|
const mspOrg1 = 'Org1MSP';
|
|
function prettyJSONString(inputString) {
|
|
if (inputString) {
|
|
return JSON.stringify(JSON.parse(inputString), null, 2);
|
|
}
|
|
else {
|
|
return inputString;
|
|
}
|
|
}
|
|
|
|
async function initContractFromOrg1Identity() {
|
|
console.log('\nFabric client user & Gateway init: Using Org1 identity to Org1 Peer');
|
|
// load the network configuration
|
|
let ccpPath = path.resolve(__dirname, '..', '..', 'test-network', 'organizations', 'peerOrganizations', 'org1.example.com', 'connection-org1.json');
|
|
let fileExists = fs.existsSync(ccpPath);
|
|
if (!fileExists) {
|
|
throw new Error(`no such file or directory: ${ccpPath}`);
|
|
}
|
|
let ccpOrg1 = JSON.parse(fs.readFileSync(ccpPath, 'utf8'));
|
|
// Create a new file system based wallet for managing identities.
|
|
const walletPathOrg1 = path.join(__dirname, 'wallet/org1');
|
|
const walletOrg1 = await Wallets.newFileSystemWallet(walletPathOrg1);
|
|
console.log(`Org1 wallet path: ${walletPathOrg1}`);
|
|
|
|
// Create a new CA client for interacting with this Orgs CA.
|
|
const caInfo = ccpOrg1.certificateAuthorities['ca.org1.example.com'];
|
|
const caTLSCACerts = caInfo.tlsCACerts.pem;
|
|
const caService = new FabricCAServices(caInfo.url, { trustedRoots: caTLSCACerts, verify: false }, caInfo.caName);
|
|
|
|
// register & enroll admin user with CA, stores admin identity in local wallet
|
|
await caUtils.EnrollOrgAdminUser(mspOrg1, walletOrg1, caService);
|
|
|
|
// register & enroll application user with CA, which is used as client identify to make chaincode calls, stores app user identity in local wallet
|
|
await caUtils.RegisterOrgUser(caUtils.Org1UserId, mspOrg1, walletOrg1, caService);
|
|
|
|
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: caUtils.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('\nFabric client user & Gateway init: Using Org2 identity to Org2 Peer');
|
|
// load the network configuration
|
|
let ccpPath = path.resolve(__dirname, '..', '..', 'test-network', 'organizations', 'peerOrganizations', 'org2.example.com', 'connection-org2.json');
|
|
let fileExists = fs.existsSync(ccpPath);
|
|
if (!fileExists) {
|
|
throw new Error(`no such file or directory: ${ccpPath}`);
|
|
}
|
|
const ccpOrg2 = JSON.parse(fs.readFileSync(ccpPath, 'utf8'));
|
|
|
|
// Create a new file system based wallet for managing identities.
|
|
const walletPathOrg2 = path.join(__dirname, 'wallet/org2');
|
|
const walletOrg2 = await Wallets.newFileSystemWallet(walletPathOrg2);
|
|
console.log(`Org2 wallet path: ${walletPathOrg2}`);
|
|
|
|
// Create a new CA client for interacting with this Orgs CA.
|
|
const caInfo = ccpOrg2.certificateAuthorities['ca.org2.example.com'];
|
|
const caTLSCACerts = caInfo.tlsCACerts.pem;
|
|
const caService = new FabricCAServices(caInfo.url, { trustedRoots: caTLSCACerts, verify: false }, caInfo.caName);
|
|
|
|
await caUtils.EnrollOrgAdminUser(mspOrg2, walletOrg2, caService);
|
|
|
|
// register & enroll application user with CA, which is used as client identify to make chaincode calls, stores app user identity in local wallet
|
|
await caUtils.RegisterOrgUser(caUtils.Org2UserId, mspOrg2, walletOrg2, caService);
|
|
|
|
try {
|
|
// Create a new gateway for connecting to Org's peer node.
|
|
const gatewayOrg2 = new Gateway();
|
|
await gatewayOrg2.connect(ccpOrg2,
|
|
{ wallet: walletOrg2, identity: caUtils.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: 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('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***********************');
|
|
console.log('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***********************');
|
|
console.log('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()));
|
|
|
|
|
|
console.log('\n~~~~~~~~~~~~~~~~ As Org2 Client ~~~~~~~~~~~~~~~~');
|
|
console.log('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('\nSubmit 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('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('Submit Transaction: TransferAsset ' + assetID1);
|
|
let buyerDetails = { assetID: assetID1, buyerMSP: mspOrg2 };
|
|
statefulTxn = contractOrg1.createTransaction('TransferAsset');
|
|
tmapData = Buffer.from(JSON.stringify(buyerDetails));
|
|
statefulTxn.setTransient({
|
|
asset_owner: tmapData
|
|
});
|
|
result = await statefulTxn.submit();
|
|
|
|
console.log('\n***********************');
|
|
//Again ReadAsset : results will show that the buyer identity now owns the asset:
|
|
console.log('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('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('Evaluate Transaction: ReadAsset ' + assetID2);
|
|
result = await contractOrg1.evaluateTransaction('ReadAsset', assetID2);
|
|
console.log(' result: ' + prettyJSONString(result.toString()));
|
|
|
|
console.log('\n***********************');
|
|
// Delete Asset2
|
|
console.log('Deleting Asset ' + assetID2);
|
|
statefulTxn = contractOrg1.createTransaction('DeleteAsset');
|
|
|
|
let dataForDelete = { assetID: assetID2 };
|
|
tmapData = Buffer.from(JSON.stringify(dataForDelete));
|
|
statefulTxn.setTransient({
|
|
asset_delete: tmapData
|
|
});
|
|
result = await statefulTxn.submit();
|
|
console.log('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('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(); |