From 4158877505d9b163c2ea10b09da5b69244473051 Mon Sep 17 00:00:00 2001 From: Simon Stone Date: Tue, 6 Aug 2019 09:14:39 +0100 Subject: [PATCH] [FAB-16232] Remove FabToken sample The FabToken sample needs to be removed, as the FabToken code itself is being removed/disabled by Angelo De Caro due to it not being included in Fabric v2.0. Additionally, the FabToken sample is subject to sporadic CI failures, which we are trying to clean up. Signed-off-by: Simon Stone Change-Id: Id434d384493f030d2407ca2f9d8687b0681a3723 --- Jenkinsfile | 21 -- fabtoken/README.md | 185 ----------------- fabtoken/javascript/.gitignore | 8 - fabtoken/javascript/fabtoken.js | 345 ------------------------------- fabtoken/javascript/package.json | 22 -- fabtoken/startFabric.sh | 46 ----- scripts/ci_scripts/ciScript.sh | 21 -- scripts/ci_scripts/fabtoken.sh | 48 ----- 8 files changed, 696 deletions(-) delete mode 100644 fabtoken/README.md delete mode 100644 fabtoken/javascript/.gitignore delete mode 100644 fabtoken/javascript/fabtoken.js delete mode 100644 fabtoken/javascript/package.json delete mode 100755 fabtoken/startFabric.sh delete mode 100755 scripts/ci_scripts/fabtoken.sh diff --git a/Jenkinsfile b/Jenkinsfile index 304305ca86..21965e5413 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -136,27 +136,6 @@ } } } - // Run fabtoken tests - stage('Run FabToken Tests') { - steps { - script { - // making the output color coded - wrap([$class: 'AnsiColorBuildWrapper', 'colorMapName': 'xterm']) { - try { - dir("$ROOTDIR/$BASE_DIR/scripts/ci_scripts") { - // Run fabtoken tests - sh './ciScript.sh --fabtoken_Tests' - } - } - catch (err) { - failure_stage = "fabtoken_Tests" - currentBuild.result = 'FAILURE' - throw err - } - } - } - } - } } // stages post { always { diff --git a/fabtoken/README.md b/fabtoken/README.md deleted file mode 100644 index c91000d99b..0000000000 --- a/fabtoken/README.md +++ /dev/null @@ -1,185 +0,0 @@ - -# FabToken Sample Application - -This is a Node.js sample application that demonstrates how to perform token operations on -a Fabric network using Fabric Node SDK. - -The sample assumes an understanding of the Hyperledger Fabric network (orderers, peers, -and channels) and of Node.js application development, including the use of the Javascript -promise, async and await. - -For more information about tokens on Hyperledger Fabric, see -[Using Fabtoken](https://hyperledger-fabric.readthedocs.io/en/latest/token/FabToken.html) - -For more information about the Fabric SDK for Node.js, refer to -[Node SDK documentation](https://fabric-sdk-node.github.io/master/index.html) - -For more information about the Node SDK TokenClient API, refer to the following: -* [TokenClient API reference](https://fabric-sdk-node.github.io/master/TokenClient.html) -* [FabToken tutorial](https://fabric-sdk-node.github.io/master/tutorial-fabtoken.html) - -## Run the sample -You can find the `fabtoken.js` sample application in the `javascript` directory. You will -use this application to create and transfer tokens on a network created using the -`basic-network` sample. First, you need to have an initial setup. - -### Setup -You will need to install version 8.9.x of Node.js and download the application dependencies. -* Change to `javascript` directory: `cd javascript` -* Run the following command to install the required packages: `npm install` - -Now you can start the network: -* Navigate back to the main `FabToken` directory: `cd ..` -* Start fabric network: `./startFabric.sh` - -This command will create a fabric network with 1 peer, an ordering service, one -channel, and two users that our application will use to issue and transfer tokens. - -### Run the app right away - -The `fabtoken.js` application includes a `demo` method that runs an end to end token flow -with hardcoded parameters. - -* Navigate to the `javascript` directory -* Run the command `node fabtoken` without any arguments to run the demo. - -You should see the output of the demo in your terminal. The demo uses user1 and user2 of -the basic network to do the following token operations: -* Issue a token worth 100 USD to user1 -* Transfer 30 USD from user1 to user2 -* Redeem 10 USD as user1 and 30 USD as user2 -* Check that user1 has a token worth 60 USD and user2 has no tokens - -### Use the sample app to create your own tokens - -You can pass arguments to `fabtoken.js` to create your own tokens and follow your own -token flow. - -#### Issue tokens - -Tokens need to be issued before they can be spent. You can use the command -`node fabtoken issue ` to issue tokens of any -type and quantity to user1 or user2. - -* As an example, the first command issues a token worth 100 US dollars to user1. The -second command issues a token worth 200 Euros to user2: - -``` -node fabtoken issue user1 USD 100 -node fabtoken issue user2 EURO 200 -``` - -#### List tokens - -After you issue tokens, you can use the list method to query the tokens that you own. Run -the command `node fabtoken list `. You need to use this command to recover the -tokenIDs that you will need to transfer or redeem your tokens. - -* As an example, you can use the command below to list the tokens owned by user1: - -``` -node fabtoken list user1 -``` -* The command returns a list of tokens, with the tokenID consisting of a tx_id and -index. You will need to use these values for future commands. - -``` -[ { id: - { tx_id: 'c9b1211d9ad809e6ee1b542de6886d8d1d9e1c938d88eff23a3ddb4e8c080e4d', - index: 0 }, - type: 'USD', - quantity: '100' } -] -``` - -* To list the tokens owned by user2, use the `node fabtoken list user2` command. - -``` -[ { id: - { tx_id: 'ab5670d3b20b6247b17a342dd2c5c4416f79db95c168beccb7d32b3dd382e5a5', - index: 0 }, - type: 'EURO', - quantity: '200' } -] -``` - -#### Transfer tokens - -Tokens can be transferred between users on a channel using the -`node fabtoken transfer ` command. -* `` and `` are the "tx_id" and "index" that you found using the list -command -* `` is the quantity to be transferred - -Any remaining quantity will be transferred back to the owner by creating a new token with -a new tokenID. -* As an example, the following command transfers 30 dollars from user1 to user2: - -``` - node fabtoken transfer user1 user2 30 c9b1211d9ad809e6ee1b542de6886d8d1d9e1c938d88eff23a3ddb4e8c080e4d 0 - ``` - -You can run the command `node fabtoken list user2` to verify that user2 now owns a new token -worth 30 dollars. You can also run the command `node fabtoken list user1` to verify that -a new token worth 70 dollars now belongs to user1. - - -#### Redeem tokens - -Tokens can be taken out of circulation by being redeemed. Redeemed tokens can no longer -be transfered to any member of the channel. Run the command -`node fabtoken redeem ` to redeem any tokens -belonging to user1 or user2. -* `` and `` are the "tx_id" and "index" returned from the list command -* `` is the quantity to be redeemed - -Any remaining quantity will be transferred back to the owner with a new tokenID. -* As an example, the following command redeems 10 Euro's belonging to user2: - -``` - node fabtoken redeem user2 10 ab5670d3b20b6247b17a342dd2c5c4416f79db95c168beccb7d32b3dd382e5a5 0 - ``` - -#### Clean up - -If you are finished using the sample application, you can bring down the network and any -accompanying artifacts. - -* Change to `fabric-samples/basic-network` directory -* To stop the network, run `./stop.sh` -* To completely remove all incriminating evidence of the network, run `./teardown.sh` - -## Understanding the `fabtoken.js` application - -You can examine the `fabtoken.js` file to get a better understanding of how the -sample application uses the FabToken APIs. - - -1. The `createFabricClient` method creates an instance of the fabric-client, and is -used to connect to the components of your network. - -2. The `createUsers` method uses the certificates generated by the basic network to -create `admin`, `user1` and `user2` users for the application. - -3. To perform token operations, you must create a `TokenClient` instance from a `Client` -object. Make sure the client has set the user context. Below is the code snippet. - -``` - // set user context to the client - await client.setUserContext(user, true); - - // create a TokenClient instance - const tokenClient = client.newTokenClient(channel, 'localhost:7051'); -``` - -4. The `issue` method creates an issue request and submits the request to issue tokens to -your network. - -5. The `list` method submits the request to list tokens of a -given owner. You will need the token IDs returned from this method to transfer or redeem tokens. - -6. The `transfer` method creates a transfer request and submits the request to transfer tokens -between users. - -7. The `redeem` method creates a redeem request and submits the request to redeem a user's -tokens. \ No newline at end of file diff --git a/fabtoken/javascript/.gitignore b/fabtoken/javascript/.gitignore deleted file mode 100644 index 3be8dfd89c..0000000000 --- a/fabtoken/javascript/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# -# SPDX-License-Identifier: Apache-2.0 -# - -# Dependency directories -node_modules/ -package-lock.json - diff --git a/fabtoken/javascript/fabtoken.js b/fabtoken/javascript/fabtoken.js deleted file mode 100644 index 54d0c6b4bd..0000000000 --- a/fabtoken/javascript/fabtoken.js +++ /dev/null @@ -1,345 +0,0 @@ -'use strict'; -/* -* Copyright IBM Corp All Rights Reserved -* -* SPDX-License-Identifier: Apache-2.0 -*/ -/* - * Chaincode Invoke - */ - -const Fabric_Client = require('fabric-client'); -const path = require('path'); -const util = require('util'); -const os = require('os'); -const fs = require('fs-extra'); - -const channel_name = "mychannel" - -start(); - -async function start() { - console.log('\n\n --- fabtoken.js - start'); - try { - console.log('Setting up client side network objects'); - - // create fabric client and related instances - // starting point for all interactions with the fabric network - const {fabric_client, channel} = createFabricClient(); - - // create users from existing crypto materials - const {admin, user1, user2} = await createUsers(); - - console.log('Successfully setup client side'); - - let operation = null; - let user = null; - const args = []; - - // if there is no argument, it will run demo by calling hardcoded token operations - // if there are arguments, it will invoke corresponding issue, list, transfer, redeem operations - if (process.argv.length == 2) { - demo(fabric_client, channel, admin, user1, user2) - return - } else if (process.argv.length >= 4) { - operation = process.argv[2]; - if (process.argv[3] === 'user1') { - user = user1; - } else if (process.argv[3] === 'user2') { - user = user2; - } else { - throw new Error(util.format('Invalid username "%s". Must be user1 or user2', process.argv[3])); - } - for (let i = 4; i < process.argv.length; i++) { - if (process.argv[i]) { - console.log(' Token arg: ' + process.argv[i]); - args.push(process.argv[i]); - } - } - } else { - throw new Error('Missing required arguments: operation, user'); - } - - console.log('\n\nStart %s token operation', operation); - let result = null; - switch (operation) { - case 'issue': - if (args.length < 2) { - throw new Error('Missing required parameter for issue: token_type, quantity'); - } - result = await issue(fabric_client, channel, admin, user, args); - break; - case 'transfer': - if (args.length < 4) { - throw new Error('Missing required parameters for transfer: recipient, transfer_quantity, tx_id, index'); - } - let recipient - if (args[0] === 'user1') { - recipient = user1; - } else if (args[0] === 'user2') { - recipient = user2; - } else { - throw new Error(util.format('Invalid recipient "%s". Must be user1 or user2', process.argv[3])); - } - // shift out args[0] because recipient object is passed separately - args.shift(); - result = await transfer(fabric_client, channel, user, recipient, args); - break; - case 'redeem': - if (args.length < 3) { - throw new Error('Missing required parameter for redeem: quantity, tx_id, index'); - } - result = await redeem(fabric_client, channel, user, args); - break; - case 'list': - result = await list(fabric_client, channel, user); - break; - default: - throw new Error(' Unknown operation requested: ' + operation); - } - - console.log('End %s token operation, returns\n %s', operation, util.inspect(result, {depth: null})); - - } catch(error) { - console.log('Problem with fabric token ::'+ error.toString()); - process.exit(1); - } - console.log('\n\n --- fabtoken.js - end'); -}; - -// demo invokes token operations using hardcoded parameters -async function demo(client, channel, admin, user1, user2) { - await reset(client, channel, user1, user2); - - console.log('admin issues token to user1, wait 5 seconds for transaction to be committed'); - await issue(client, channel, admin, user1, ['USD', '100']); - await sleep(5000) - - let user1_tokens = await list(client, channel, user1); - console.log('\nuser1 has a token in USD type and 100 quantity after issue:\n%s', util.inspect(user1_tokens, {depth: null})); - - console.log('\nuser1 transfers 30 quantity of the token to user2, wait 5 seconds for transaction to be committed'); - let token_id = user1_tokens[0].id; - await transfer(client, channel, user1, user2, ['30', token_id.tx_id, token_id.index]); - await sleep(5000) - - user1_tokens = await list(client, channel, user1); - console.log('\nuser1 has a token in 70 quantity after transfer:\n%s', util.inspect(user1_tokens, {depth: null})); - - let user2_tokens = await list(client, channel, user2); - console.log('\nuser2 has a token in 30 quantity after transfer:\n%s', util.inspect(user2_tokens, {depth: null})); - - console.log('\nuser1 redeems 10 out of 70 quantity of the token'); - token_id = user1_tokens[0].id; - await redeem(client, channel, user1, ['10', token_id.tx_id, token_id.index]); - - console.log('\nuser2 redeems entire token, wait 5 seconds for transaction to be committed'); - token_id = user2_tokens[0].id; - await redeem(client, channel, user2, ['30', token_id.tx_id, token_id.index]); - await sleep(5000) - - user1_tokens = await list(client, channel, user1); - console.log('\nuser1 has a token in 60 quantity after redeem:\n%s', util.inspect(user1_tokens, {depth: null})); - - user2_tokens = await list(client, channel, user2); - console.log('\nuser2 has no token after redeem:\n%s', util.inspect(user2_tokens, {depth: null})); - - await reset(client, channel, user1, user2); -} - -// reset removes all the existing tokens on the channel to get a fresh env -async function reset(client, channel, user1, user2) { - console.log('\nReset: remove all the tokens on the channel\n'); - - let tokens = await list(client, channel, user1); - for (const token of tokens) { - await redeem(client, channel, user1, [token.quantity, token.id.tx_id, token.id.index]); - } - - tokens = await list(client, channel, user2); - for (const token of tokens) { - await redeem(client, channel, user2, [token.quantity, token.id.tx_id, token.id.index]); - } -} - -// Issue token to the user with args [type, quantity] -// It uses "admin" to issue tokens, but other users can also issue tokens as long as they have the permission. -async function issue(client, channel, admin, user, args) { - console.log('Start token issue with args ' + args); - - await client.setUserContext(admin, true); - - const tokenClient = client.newTokenClient(channel, 'localhost:7051'); - - // build the request to issue tokens to the user - const txId = client.newTransactionID(); - const param = { - owner: user.getIdentity().serialize(), - type: args[0], - quantity: args[1] - }; - const request = { - params: [param], - txId: txId, - }; - - return await tokenClient.issue(request); -} - -// Transfers token from the user to the recipient with args [quantity, tx_id, index] -async function transfer(client, channel, user, recipient, args) { - console.log('Start token transfer with args ' + args); - - await client.setUserContext(user, true); - - const tokenClient = client.newTokenClient(channel, 'localhost:7051'); - - // build the request to transfer tokens to the recipient - const txId = client.newTransactionID(); - const param1 = { - owner: recipient.getIdentity().serialize(), - quantity: args[0] - }; - - const request = { - tokenIds: [{tx_id: args[1], index: parseInt(args[2])}], - params: [param1], - txId: txId, - }; - - return await tokenClient.transfer(request); -} - -// Redeem tokens from the user with args [quantity, tx_id, index] -async function redeem(client, channel, user, args) { - console.log('Start token redeem with args ' + args); - - await client.setUserContext(user, true); - - const tokenClient = client.newTokenClient(channel, 'localhost:7051'); - - // build the request to redeem tokens - const txId = client.newTransactionID(); - const param = { - quantity: args[0] - }; - const request = { - tokenIds: [{tx_id: args[1], index: parseInt(args[2])}], - params: [param], - txId: txId, - }; - - return await tokenClient.redeem(request); -} - -// List tokens for the user -async function list(client, channel, user) { - await client.setUserContext(user, true); - - const tokenClient = client.newTokenClient(channel, 'localhost:7051'); - - return await tokenClient.list(); -} - -// Create fabric client, channel, orderer, and peer instances. -// These are needed for SDK to invoke token operations. -function createFabricClient() { - // fabric client instance - // starting point for all interactions with the fabric network - const fabric_client = new Fabric_Client(); - - // -- channel instance to represent the ledger - const channel = fabric_client.newChannel(channel_name); - console.log(' Created client side object to represent the channel'); - - // -- peer instance to represent a peer on the channel - const peer = fabric_client.newPeer('grpc://localhost:7051'); - console.log(' Created client side object to represent the peer'); - - // -- orderer instance to reprsent the channel's orderer - const orderer = fabric_client.newOrderer('grpc://localhost:7050') - console.log(' Created client side object to represent the orderer'); - - // add peer and orderer to the channel - channel.addPeer(peer); - channel.addOrderer(orderer); - - return {fabric_client: fabric_client, channel: channel}; -} - -// Create admin, user1 and user2 by loading crypto files -async function createUsers() { - // This sample application will read user idenitity information from - // pre-generated crypto files and create users. It will use a client object as - // an easy way to create the user objects from known cyrpto material. - - const client = new Fabric_Client(); - - // load admin - let keyPath = path.join(__dirname, '../../basic-network/crypto-config/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp/keystore'); - let keyPEM = Buffer.from(readAllFiles(keyPath)[0]).toString(); - let certPath = path.join(__dirname, '../../basic-network/crypto-config/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp/signcerts'); - let certPEM = readAllFiles(certPath)[0]; - - let user_opts = { - username: 'admin', - mspid: 'Org1MSP', - skipPersistence: true, - cryptoContent: { - privateKeyPEM: keyPEM, - signedCertPEM: certPEM - } - }; - const admin = await client.createUser(user_opts); - - // load user1 - keyPath = path.join(__dirname, '../../basic-network/crypto-config/peerOrganizations/org1.example.com/users/User1@org1.example.com/msp/keystore'); - keyPEM = Buffer.from(readAllFiles(keyPath)[0]).toString(); - certPath = path.join(__dirname, '../../basic-network/crypto-config/peerOrganizations/org1.example.com/users/User1@org1.example.com/msp/signcerts'); - certPEM = readAllFiles(certPath)[0]; - - user_opts = { - username: 'user1', - mspid: 'Org1MSP', - skipPersistence: true, - cryptoContent: { - privateKeyPEM: keyPEM, - signedCertPEM: certPEM - } - }; - const user1 = await client.createUser(user_opts); - - // load user2 - keyPath = path.join(__dirname, '../../basic-network/crypto-config/peerOrganizations/org1.example.com/users/User2@org1.example.com/msp/keystore'); - keyPEM = Buffer.from(readAllFiles(keyPath)[0]).toString(); - certPath = path.join(__dirname, '../../basic-network/crypto-config/peerOrganizations/org1.example.com/users/User2@org1.example.com/msp/signcerts'); - certPEM = readAllFiles(certPath)[0]; - - user_opts = { - username: 'user2', - mspid: 'Org1MSP', - skipPersistence: true, - cryptoContent: { - privateKeyPEM: keyPEM, - signedCertPEM: certPEM - } - }; - const user2 = await client.createUser(user_opts); - - return {admin: admin, user1: user1, user2: user2}; -} - -function readAllFiles(dir) { - const files = fs.readdirSync(dir); - const certs = []; - files.forEach((file_name) => { - const file_path = path.join(dir, file_name); - const data = fs.readFileSync(file_path); - certs.push(data); - }); - return certs; -} - -function sleep(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); -} diff --git a/fabtoken/javascript/package.json b/fabtoken/javascript/package.json deleted file mode 100644 index 433febde5e..0000000000 --- a/fabtoken/javascript/package.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "fabtoken", - "version": "1.0.0", - "description": "Hyperledger Fabric Token Sample Application", - "main": "fabtoken.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "dependencies": { - "fabric-client": "unstable", - "fs-extra": "^6.0.1", - "util": "^0.10.3" - }, - "license": "Apache-2.0", - "keywords": [ - "Hyperledger", - "Fabric", - "Token", - "Sample", - "Application" - ] -} diff --git a/fabtoken/startFabric.sh b/fabtoken/startFabric.sh deleted file mode 100755 index 664f8c3fb1..0000000000 --- a/fabtoken/startFabric.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/bin/bash -# -# Copyright IBM Corp All Rights Reserved -# -# SPDX-License-Identifier: Apache-2.0 -# -# Exit on first error -set -e - -# don't rewrite paths for Windows Git Bash users -export MSYS_NO_PATHCONV=1 -starttime=$(date +%s) - -# launch network; create channel and join peer to channel -cd ../basic-network -./start.sh - -cat < - - example: node fabtoken issue user1 USD 100 - node fabtoken list - - example: node fabtoken list user1 - - select a token to transfer or redeem and pass "tx_id" and "index" as input parameters - node fabtoken transfer - - example: node fabtoken transfer user1 user2 30 c9b1211d9ad809e6ee1b542de6886d8d1d9e1c938d88eff23a3ddb4e8c080e4d 0 - - and are the "tx_id" and "index" returned from the list operation that specifies the token id for transfer - node fabtoken redeem - - example: node fabtoken redeem user2 10 477c7bf2002814497c228fd8cbc4d80c8b7f1602b2c17ffadb6cf7e5783fa47a 0 - - and are the "tx_id" and "index" returned from the list operation - -EOF diff --git a/scripts/ci_scripts/ciScript.sh b/scripts/ci_scripts/ciScript.sh index 8b2ffa03b4..f2edebf10a 100755 --- a/scripts/ci_scripts/ciScript.sh +++ b/scripts/ci_scripts/ciScript.sh @@ -16,9 +16,6 @@ Parse_Arguments() { --fabcar_Tests) fabcar_Tests ;; - --fabtoken_Tests) - fabtoken_Tests - ;; esac shift done @@ -54,22 +51,4 @@ fabcar_Tests() { ./fabcar.sh } -# run fabtoken tests -fabtoken_Tests() { - - echo " #############################" - echo "npm version ------> $(npm -v)" - echo "node version ------> $(node -v)" - echo " #############################" - - echo - echo " _____ _ ____ _______ __ ___ __ _____ __ __ " - echo " | ___| / \ | __ ) |__ __| / _ \ | | / / | ____| | |\ | | " - echo " | |_ / _ \ | _ \ | | | | | | | |/ / | |___ | | \ | | " - echo " | _| / ___ \ | |_) | | | | |_ | | | |\ \ | ___| | | \ | | " - echo " |_| /_/ \_\ |____/ |_| \ __ / |_| \_\ |_|____ |_| \|_| " - - ./fabtoken.sh -} - Parse_Arguments $@ diff --git a/scripts/ci_scripts/fabtoken.sh b/scripts/ci_scripts/fabtoken.sh deleted file mode 100755 index 336c92cf9e..0000000000 --- a/scripts/ci_scripts/fabtoken.sh +++ /dev/null @@ -1,48 +0,0 @@ -#!/bin/bash -# -# SPDX-License-Identifier: Apache-2.0 -# - -# docker container list - Check these from basic-network/docker-compose.yaml -CONTAINER_LIST=(peer0.org1 orderer ca) - -logs() { - -for CONTAINER in ${CONTAINER_LIST[*]}; do - docker logs $CONTAINER.example.com >& $WORKSPACE/$CONTAINER-$1.log - echo -done -# Write couchdb container logs into couchdb.log file -docker logs couchdb >& couchdb.log - -} - -copy_logs() { - -# Call logs function -logs $2 $3 - -if [ $1 != 0 ]; then - echo -e "\033[31m $2 test case is FAILED" "\033[0m" - exit 1 -fi -} - -cd $WORKSPACE/$BASE_DIR/fabtoken || exit -export PATH=gopath/src/github.com/hyperledger/fabric-samples/bin:$PATH - -LANGUAGE="javascript" - -echo -e "\033[32m starting fabtoken test (${LANGUAGE})" "\033[0m" -./startFabric.sh -copy_logs $? fabtoken-start-script-${LANGUAGE} - -pushd ${LANGUAGE} -npm install -node fabtoken.js -copy_logs $? fabtoken-${LANGUAGE} -popd - -docker ps -aq | xargs docker rm -f -docker rmi -f $(docker images -aq dev-*) -echo -e "\033[32m finished fabtoken tests (${LANGUAGE})" "\033[0m"