mirror of
https://github.com/hyperledger/fabric-samples.git
synced 2026-06-17 15:35:09 +00:00
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:
parent
9f245809bb
commit
33adb8d164
10 changed files with 3068 additions and 1 deletions
|
|
@ -25,7 +25,7 @@ transfer an asset in a more realistic transfer scenario.
|
|||
|
||||
| **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 |
|
||||
| [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 |
|
||||
|
|
|
|||
15
asset-transfer-basic/application-typescript/.gitignore
vendored
Normal file
15
asset-transfer-basic/application-typescript/.gitignore
vendored
Normal 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
|
||||
|
||||
2601
asset-transfer-basic/application-typescript/package-lock.json
generated
Normal file
2601
asset-transfer-basic/application-typescript/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
50
asset-transfer-basic/application-typescript/package.json
Normal file
50
asset-transfer-basic/application-typescript/package.json
Normal 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
|
||||
}
|
||||
}
|
||||
171
asset-transfer-basic/application-typescript/src/app.ts
Normal file
171
asset-transfer-basic/application-typescript/src/app.ts
Normal 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();
|
||||
|
|
@ -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,
|
||||
};
|
||||
104
asset-transfer-basic/application-typescript/src/utils/CAUtil.ts
Normal file
104
asset-transfer-basic/application-typescript/src/utils/CAUtil.ts
Normal 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,
|
||||
};
|
||||
19
asset-transfer-basic/application-typescript/tsconfig.json
Normal file
19
asset-transfer-basic/application-typescript/tsconfig.json
Normal 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"
|
||||
]
|
||||
}
|
||||
23
asset-transfer-basic/application-typescript/tslint.json
Normal file
23
asset-transfer-basic/application-typescript/tslint.json
Normal 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": []
|
||||
}
|
||||
|
|
@ -50,3 +50,15 @@ print "Executing app.js"
|
|||
node app.js
|
||||
popd
|
||||
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
|
||||
|
|
|
|||
Loading…
Reference in a new issue