asset transfer basic - application typescript (#339)

Signed-off-by: Rijul Aggarwal <rijul.aggarwal@ibm.com>

Co-authored-by: Rijul Aggarwal <rijul.aggarwal@ibm.com>
This commit is contained in:
Rijul Aggarwal 2020-11-04 10:27:09 -05:00 committed by GitHub
parent 9f245809bb
commit 33adb8d164
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 3068 additions and 1 deletions

View file

@ -25,7 +25,7 @@ transfer an asset in a more realistic transfer scenario.
| **Smart Contract** | **Description** | **Tutorial** | **Smart contract languages** | **Application languages** | | **Smart Contract** | **Description** | **Tutorial** | **Smart contract languages** | **Application languages** |
| -----------|------------------------------|----------|---------|---------| | -----------|------------------------------|----------|---------|---------|
| [Basic](asset-transfer-basic) | The Basic sample smart contract that allows you to create and transfer an asset by putting data on the ledger and retrieving it. This sample is recommended for new Fabric users. | [Writing your first application](https://hyperledger-fabric.readthedocs.io/en/latest/write_first_app.html) | Go, JavaScript, TypeScript, Java | Go, JavaScript, Java | | [Basic](asset-transfer-basic) | The Basic sample smart contract that allows you to create and transfer an asset by putting data on the ledger and retrieving it. This sample is recommended for new Fabric users. | [Writing your first application](https://hyperledger-fabric.readthedocs.io/en/latest/write_first_app.html) | Go, JavaScript, TypeScript, Java | Go, JavaScript, TypeScript, Java |
| [Ledger queries](asset-transfer-ledger-queries) | The ledger queries sample demonstrates range queries and transaction updates using range queries (applicable for both LevelDB and CouchDB state databases), and how to deploy an index with your chaincode to support JSON queries (applicable for CouchDB state database only). | [Using CouchDB](https://hyperledger-fabric.readthedocs.io/en/latest/couchdb_tutorial.html) | Go, JavaScript | Java, JavaScript | | [Ledger queries](asset-transfer-ledger-queries) | The ledger queries sample demonstrates range queries and transaction updates using range queries (applicable for both LevelDB and CouchDB state databases), and how to deploy an index with your chaincode to support JSON queries (applicable for CouchDB state database only). | [Using CouchDB](https://hyperledger-fabric.readthedocs.io/en/latest/couchdb_tutorial.html) | Go, JavaScript | Java, JavaScript |
| [Private data](asset-transfer-private-data) | This sample demonstrates the use of private data collections, how to manage private data collections with the chaincode lifecycle, and how the private data hash can be used to verify private data on the ledger. It also demonstrates how to control asset updates and transfers using client-based ownership and access control. | [Using Private Data](https://hyperledger-fabric.readthedocs.io/en/latest/private_data_tutorial.html) | Go | JavaScript | | [Private data](asset-transfer-private-data) | This sample demonstrates the use of private data collections, how to manage private data collections with the chaincode lifecycle, and how the private data hash can be used to verify private data on the ledger. It also demonstrates how to control asset updates and transfers using client-based ownership and access control. | [Using Private Data](https://hyperledger-fabric.readthedocs.io/en/latest/private_data_tutorial.html) | Go | JavaScript |
| [State-Based Endorsement](asset-transfer-sbe) | This sample demonstrates how to override the chaincode-level endorsement policy to set endorsement policies at the key-level (data/asset level). | [Using State-based endorsement](https://github.com/hyperledger/fabric-samples/tree/master/asset-transfer-sbe) | Java, TypeScript | JavaScript | | [State-Based Endorsement](asset-transfer-sbe) | This sample demonstrates how to override the chaincode-level endorsement policy to set endorsement policies at the key-level (data/asset level). | [Using State-based endorsement](https://github.com/hyperledger/fabric-samples/tree/master/asset-transfer-sbe) | Java, TypeScript | JavaScript |

View file

@ -0,0 +1,15 @@
#
# SPDX-License-Identifier: Apache-2.0
#
# Coverage directory used by tools like istanbul
coverage
# Dependency directories
node_modules/
jspm_packages/
# Compiled TypeScript files
dist

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,50 @@
{
"name": "asset-transfer-basic",
"version": "1.0.0",
"description": "Asset Transfer Basic contract implemented in TypeScript",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
"engines": {
"node": ">=12",
"npm": ">=5"
},
"scripts": {
"lint": "tslint -c tslint.json 'src/**/*.ts'",
"pretest": "npm run lint",
"start": "npm run build && node dist/app.js",
"build": "tsc",
"build:watch": "tsc -w",
"prepublishOnly": "npm run build"
},
"engineStrict": true,
"author": "Hyperledger",
"license": "Apache-2.0",
"dependencies": {
"fabric-ca-client": "^2.2.0",
"fabric-network": "^2.2.0"
},
"devDependencies": {
"tslint": "^5.11.0",
"typescript": "^3.1.6"
},
"nyc": {
"extension": [
".ts",
".tsx"
],
"exclude": [
"coverage/**",
"dist/**"
],
"reporter": [
"text-summary",
"html"
],
"all": true,
"check-coverage": true,
"statements": 100,
"branches": 100,
"functions": 100,
"lines": 100
}
}

View file

@ -0,0 +1,171 @@
/*
* Copyright IBM Corp. All Rights Reserved.
*
* SPDX-License-Identifier: Apache-2.0
*/
import { Gateway, GatewayOptions } from 'fabric-network';
import * as path from 'path';
import { buildCCPOrg1, buildWallet, prettyJSONString } from './utils//AppUtil';
import { buildCAClient, enrollAdmin, registerAndEnrollUser } from './utils/CAUtil';
const channelName = 'mychannel';
const chaincodeName = 'basic';
const mspOrg1 = 'Org1MSP';
const walletPath = path.join(__dirname, 'wallet');
const org1UserId = 'appUser';
// pre-requisites:
// - fabric-sample two organization test-network setup with two peers, ordering service,
// and 2 certificate authorities
// ===> from directory /fabric-samples/test-network
// ./network.sh up createChannel -ca
// - Use any of the asset-transfer-basic chaincodes deployed on the channel "mychannel"
// with the chaincode name of "basic". The following deploy command will package,
// install, approve, and commit the javascript chaincode, all the actions it takes
// to deploy a chaincode to a channel.
// ===> from directory /fabric-samples/test-network
// ./network.sh deployCC -ccn basic -ccl javascript
// - Be sure that node.js is installed
// ===> from directory /fabric-samples/asset-transfer-basic/application-typescript
// node -v
// - npm installed code dependencies
// ===> from directory /fabric-samples/asset-transfer-basic/application-typescript
// npm install
// - to run this test application
// ===> from directory /fabric-samples/asset-transfer-basic/application-typescript
// npm start
// NOTE: If you see kind an error like these:
/*
2020-08-07T20:23:17.590Z - error: [DiscoveryService]: send[mychannel] - Channel:mychannel received discovery error:access denied
******** FAILED to run the application: Error: DiscoveryService: mychannel error: access denied
OR
Failed to register user : Error: fabric-ca request register failed with errors [[ { code: 20, message: 'Authentication failure' } ]]
******** FAILED to run the application: Error: Identity not found in wallet: appUser
*/
// Delete the /fabric-samples/asset-transfer-basic/application-typescript/wallet directory
// and retry this application.
//
// The certificate authority must have been restarted and the saved certificates for the
// admin and application user are not valid. Deleting the wallet store will force these to be reset
// with the new certificate authority.
//
/**
* A test application to show basic queries operations with any of the asset-transfer-basic chaincodes
* -- How to submit a transaction
* -- How to query and check the results
*
* To see the SDK workings, try setting the logging to show on the console before running
* export HFC_LOGGING='{"debug":"console"}'
*/
async function main() {
try {
// build an in memory object with the network configuration (also known as a connection profile)
const ccp = buildCCPOrg1();
// build an instance of the fabric ca services client based on
// the information in the network configuration
const caClient = buildCAClient(ccp, 'ca.org1.example.com');
// setup the wallet to hold the credentials of the application user
const wallet = await buildWallet(walletPath);
// in a real application this would be done on an administrative flow, and only once
await enrollAdmin(caClient, wallet, mspOrg1);
// 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(caClient, wallet, mspOrg1, org1UserId, 'org1.department1');
// Create a new gateway instance for interacting with the fabric network.
// In a real application this would be done as the backend server session is setup for
// a user that has been verified.
const gateway = new Gateway();
const gatewayOpts: GatewayOptions = {
wallet,
identity: org1UserId,
discovery: { enabled: true, asLocalhost: true }, // using asLocalhost as this gateway is using a fabric network deployed locally
};
try {
// setup the gateway instance
// The user will now be able to create connections to the fabric network and be able to
// submit transactions and query. All transactions submitted by this gateway will be
// signed by this user using the credentials stored in the wallet.
await gateway.connect(ccp, gatewayOpts);
// Build a network instance based on the channel where the smart contract is deployed
const network = await gateway.getNetwork(channelName);
// Get the contract from the network.
const contract = network.getContract(chaincodeName);
// Initialize a set of asset data on the channel using the chaincode 'InitLedger' function.
// This type of transaction would only be run once by an application the first time it was started after it
// deployed the first time. Any updates to the chaincode deployed later would likely not need to run
// an "init" type function.
console.log('\n--> Submit Transaction: InitLedger, function creates the initial set of assets on the ledger');
await contract.submitTransaction('InitLedger');
console.log('*** Result: committed');
// Let's try a query type operation (function).
// This will be sent to just one peer and the results will be shown.
console.log('\n--> Evaluate Transaction: GetAllAssets, function returns all the current assets on the ledger');
let result = await contract.evaluateTransaction('GetAllAssets');
console.log(`*** Result: ${prettyJSONString(result.toString())}`);
// Now let's try to submit a transaction.
// This will be sent to both peers and if both peers endorse the transaction, the endorsed proposal will be sent
// to the orderer to be committed by each of the peer's to the channel ledger.
console.log('\n--> Submit Transaction: CreateAsset, creates new asset with ID, color, owner, size, and appraisedValue arguments');
await contract.submitTransaction('CreateAsset', 'asset13', 'yellow', '5', 'Tom', '1300');
console.log('*** Result: committed');
console.log('\n--> Evaluate Transaction: ReadAsset, function returns an asset with a given assetID');
result = await contract.evaluateTransaction('ReadAsset', 'asset13');
console.log(`*** Result: ${prettyJSONString(result.toString())}`);
console.log('\n--> Evaluate Transaction: AssetExists, function returns "true" if an asset with given assetID exist');
result = await contract.evaluateTransaction('AssetExists', 'asset1');
console.log(`*** Result: ${prettyJSONString(result.toString())}`);
console.log('\n--> Submit Transaction: UpdateAsset asset1, change the appraisedValue to 350');
await contract.submitTransaction('UpdateAsset', 'asset1', 'blue', '5', 'Tomoko', '350');
console.log('*** Result: committed');
console.log('\n--> Evaluate Transaction: ReadAsset, function returns "asset1" attributes');
result = await contract.evaluateTransaction('ReadAsset', 'asset1');
console.log(`*** Result: ${prettyJSONString(result.toString())}`);
try {
// How about we try a transactions where the executing chaincode throws an error
// Notice how the submitTransaction will throw an error containing the error thrown by the chaincode
console.log('\n--> Submit Transaction: UpdateAsset asset70, asset70 does not exist and should return an error');
await contract.submitTransaction('UpdateAsset', 'asset70', 'blue', '5', 'Tomoko', '300');
console.log('******** FAILED to return an error');
} catch (error) {
console.log(`*** Successfully caught the error: \n ${error}`);
}
console.log('\n--> Submit Transaction: TransferAsset asset1, transfer to new owner of Tom');
await contract.submitTransaction('TransferAsset', 'asset1', 'Tom');
console.log('*** Result: committed');
console.log('\n--> Evaluate Transaction: ReadAsset, function returns "asset1" attributes');
result = await contract.evaluateTransaction('ReadAsset', 'asset1');
console.log(`*** Result: ${prettyJSONString(result.toString())}`);
} finally {
// Disconnect from the gateway when the application is closing
// This will close all connections to the network
gateway.disconnect();
}
} catch (error) {
console.error(`******** FAILED to run the application: ${error}`);
}
}
main();

View file

@ -0,0 +1,72 @@
/*
* Copyright IBM Corp. All Rights Reserved.
*
* SPDX-License-Identifier: Apache-2.0
*/
import { Wallet, Wallets } from 'fabric-network';
import * as fs from 'fs';
import * as path from 'path';
const buildCCPOrg1 = (): Record<string, any> => {
// load the common connection configuration file
const ccpPath = path.resolve(__dirname, '..', '..', '..', '..', 'test-network',
'organizations', 'peerOrganizations', 'org1.example.com', 'connection-org1.json');
const fileExists = fs.existsSync(ccpPath);
if (!fileExists) {
throw new Error(`no such file or directory: ${ccpPath}`);
}
const contents = fs.readFileSync(ccpPath, 'utf8');
// build a JSON object from the file contents
const ccp = JSON.parse(contents);
console.log(`Loaded the network configuration located at ${ccpPath}`);
return ccp;
};
const buildCCPOrg2 = (): Record<string, any> => {
// load the common connection configuration file
const ccpPath = path.resolve(__dirname, '..', '..', '..', '..', 'test-network',
'organizations', 'peerOrganizations', 'org2.example.com', 'connection-org2.json');
const fileExists = fs.existsSync(ccpPath);
if (!fileExists) {
throw new Error(`no such file or directory: ${ccpPath}`);
}
const contents = fs.readFileSync(ccpPath, 'utf8');
// build a JSON object from the file contents
const ccp = JSON.parse(contents);
console.log(`Loaded the network configuration located at ${ccpPath}`);
return ccp;
};
const buildWallet = async (walletPath: string): Promise<Wallet> => {
// Create a new wallet : Note that wallet is for managing identities.
let wallet: Wallet;
if (walletPath) {
wallet = await Wallets.newFileSystemWallet(walletPath);
console.log(`Built a file system wallet at ${walletPath}`);
} else {
wallet = await Wallets.newInMemoryWallet();
console.log('Built an in memory wallet');
}
return wallet;
};
const prettyJSONString = (inputString: string): string => {
if (inputString) {
return JSON.stringify(JSON.parse(inputString), null, 2);
} else {
return inputString;
}
};
export {
buildCCPOrg1,
buildCCPOrg2,
buildWallet,
prettyJSONString,
};

View file

@ -0,0 +1,104 @@
/*
* Copyright IBM Corp. All Rights Reserved.
*
* SPDX-License-Identifier: Apache-2.0
*/
import * as FabricCAServices from 'fabric-ca-client';
import { Wallet } from 'fabric-network';
const adminUserId = 'admin';
const adminUserPasswd = 'adminpw';
/**
*
* @param {*} ccp
*/
const buildCAClient = (ccp: Record<string, any>, caHostName: string): FabricCAServices => {
// Create a new CA client for interacting with the CA.
const caInfo = ccp.certificateAuthorities[caHostName]; // lookup CA details from config
const caTLSCACerts = caInfo.tlsCACerts.pem;
const caClient = new FabricCAServices(caInfo.url, { trustedRoots: caTLSCACerts, verify: false }, caInfo.caName);
console.log(`Built a CA Client named ${caInfo.caName}`);
return caClient;
};
const enrollAdmin = async (caClient: FabricCAServices, wallet: Wallet, orgMspId: string): Promise<void> => {
try {
// Check to see if we've already enrolled the admin user.
const identity = await wallet.get(adminUserId);
if (identity) {
console.log('An identity for the admin user already exists in the wallet');
return;
}
// Enroll the admin user, and import the new identity into the wallet.
const enrollment = await caClient.enroll({ enrollmentID: adminUserId, enrollmentSecret: adminUserPasswd });
const x509Identity = {
credentials: {
certificate: enrollment.certificate,
privateKey: enrollment.key.toBytes(),
},
mspId: orgMspId,
type: 'X.509',
};
await wallet.put(adminUserId, x509Identity);
console.log('Successfully enrolled admin user and imported it into the wallet');
} catch (error) {
console.error(`Failed to enroll admin user : ${error}`);
}
};
const registerAndEnrollUser = async (caClient: FabricCAServices, wallet: Wallet, orgMspId: string, userId: string, affiliation: string): Promise<void> => {
try {
// Check to see if we've already enrolled the user
const userIdentity = await wallet.get(userId);
if (userIdentity) {
console.log(`An identity for the user ${userId} already exists in the wallet`);
return;
}
// Must use an admin to register a new user
const adminIdentity = await wallet.get(adminUserId);
if (!adminIdentity) {
console.log('An identity for the admin user does not exist in the wallet');
console.log('Enroll the admin user before retrying');
return;
}
// build a user object for authenticating with the CA
const provider = wallet.getProviderRegistry().getProvider(adminIdentity.type);
const adminUser = await provider.getUserContext(adminIdentity, adminUserId);
// Register the user, enroll the user, and import the new identity into the wallet.
// if affiliation is specified by client, the affiliation value must be configured in CA
const secret = await caClient.register({
affiliation,
enrollmentID: userId,
role: 'client',
}, adminUser);
const enrollment = await caClient.enroll({
enrollmentID: userId,
enrollmentSecret: secret,
});
const x509Identity = {
credentials: {
certificate: enrollment.certificate,
privateKey: enrollment.key.toBytes(),
},
mspId: orgMspId,
type: 'X.509',
};
await wallet.put(userId, x509Identity);
console.log(`Successfully registered and enrolled user ${userId} and imported it into the wallet`);
} catch (error) {
console.error(`Failed to register user : ${error}`);
}
};
export {
buildCAClient,
enrollAdmin,
registerAndEnrollUser,
};

View file

@ -0,0 +1,19 @@
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"outDir": "dist",
"target": "es2017",
"moduleResolution": "node",
"module": "commonjs",
"declaration": true,
"sourceMap": true,
"noImplicitAny": true
},
"include": [
"./src/**/*"
],
"exclude": [
"./src/**/*.spec.ts"
]
}

View file

@ -0,0 +1,23 @@
{
"defaultSeverity": "error",
"extends": [
"tslint:recommended"
],
"jsRules": {},
"rules": {
"indent": [true, "spaces", 4],
"linebreak-style": [true, "LF"],
"quotemark": [true, "single"],
"semicolon": [true, "always"],
"no-console": false,
"curly": true,
"triple-equals": true,
"no-string-throw": true,
"no-var-keyword": true,
"no-trailing-whitespace": true,
"object-literal-key-quotes": [true, "as-needed"],
"object-literal-sort-keys": false,
"max-line-length": false
},
"rulesDirectory": []
}

View file

@ -50,3 +50,15 @@ print "Executing app.js"
node app.js node app.js
popd popd
stopNetwork stopNetwork
# Run typescript application
createNetwork
print "Initializing Typescript application"
pushd ../asset-transfer-basic/application-typescript
npm install
print "Building app.ts"
npm run build
print "Running the output app"
node dist/app.js
popd
stopNetwork