From 3d2875c180c67d9f3b3a166bb15b9c73fb6c7091 Mon Sep 17 00:00:00 2001 From: user00000001 Date: Wed, 16 Sep 2020 22:48:39 +0800 Subject: [PATCH 01/38] Fixed script bugs "SendTransaction failed: orderers is nil". (#326) Signed-off-by: jiangyunbin Co-authored-by: jiangyunbin --- test-network/configtx/configtx.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test-network/configtx/configtx.yaml b/test-network/configtx/configtx.yaml index d280ccbc..ad72c1e7 100644 --- a/test-network/configtx/configtx.yaml +++ b/test-network/configtx/configtx.yaml @@ -217,6 +217,14 @@ Orderer: &OrdererDefaults # Orderer Type: The orderer implementation to start OrdererType: etcdraft + + # Addresses used to be the list of orderer addresses that clients and peers + # could connect to. However, this does not allow clients to associate orderer + # addresses and orderer organizations which can be useful for things such + # as TLS validation. The preferred way to specify orderer addresses is now + # to include the OrdererEndpoints item in your org definition + Addresses: + - orderer.example.com:7050 EtcdRaft: Consenters: From f497793e57917429ab283947e2209813f22d1769 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2020 15:20:28 -0400 Subject: [PATCH 02/38] Bump yargs-parser from 13.1.1 to 13.1.2 in /fabcar/javascript (#328) Bumps [yargs-parser](https://github.com/yargs/yargs-parser) from 13.1.1 to 13.1.2. - [Release notes](https://github.com/yargs/yargs-parser/releases) - [Changelog](https://github.com/yargs/yargs-parser/blob/master/docs/CHANGELOG-full.md) - [Commits](https://github.com/yargs/yargs-parser/commits) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- fabcar/javascript/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fabcar/javascript/package-lock.json b/fabcar/javascript/package-lock.json index 065751b8..ab6f7cc9 100644 --- a/fabcar/javascript/package-lock.json +++ b/fabcar/javascript/package-lock.json @@ -3384,9 +3384,9 @@ } }, "yargs-parser": { - "version": "13.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz", - "integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==", + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", "dev": true, "requires": { "camelcase": "^5.0.0", From ed598f733bf7dfbb09c04aa214941b51766b2fa7 Mon Sep 17 00:00:00 2001 From: nikhil550 Date: Thu, 17 Sep 2020 14:27:21 -0400 Subject: [PATCH 03/38] Update help text for colors on mac (#337) Signed-off-by: NIKHIL E GUPTA --- test-network/network.sh | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/test-network/network.sh b/test-network/network.sh index 4f3818c1..a2190e40 100755 --- a/test-network/network.sh +++ b/test-network/network.sh @@ -23,15 +23,15 @@ function printHelp() { println "Usage: " println " network.sh [Flags]" println " Modes:" - println " \e[0;32mup\e[0m - bring up fabric orderer and peer nodes. No channel is created" - println " \e[0;32mup createChannel\e[0m - bring up fabric network with one channel" - println " \e[0;32mcreateChannel\e[0m - create and join a channel after the network is created" - println " \e[0;32mdeployCC\e[0m - deploy the asset transfer basic chaincode on the channel or specify" - println " \e[0;32mdown\e[0m - clear the network with docker-compose down" - println " \e[0;32mrestart\e[0m - restart the network" + println " \033[0;32mup\033[0m - bring up fabric orderer and peer nodes. No channel is created" + println " \033[0;32mup createChannel\033[0m - bring up fabric network with one channel" + println " \033[0;32mcreateChannel\033[0m - create and join a channel after the network is created" + println " \033[0;32mdeployCC\033[0m - deploy the asset transfer basic chaincode on the channel or specify" + println " \033[0;32mdown\033[0m - clear the network with docker-compose down" + println " \033[0;32mrestart\033[0m - restart the network" println println " Flags:" - println " Used with \e[0;32mnetwork.sh up\e[0m, \e[0;32mnetwork.sh createChannel\e[0m:" + println " Used with \033[0;32mnetwork.sh up\033[0m, \033[0;32mnetwork.sh createChannel\033[0m:" println " -ca - create Certificate Authorities to generate the crypto material" println " -c - channel name to use (defaults to \"mychannel\")" println " -s - the database backend to use: goleveldb (default) or couchdb" @@ -40,7 +40,7 @@ function printHelp() { println " -i - the tag to be used to launch the network (defaults to \"latest\")" println " -cai - the image tag to be used for CA (defaults to \"${CA_IMAGETAG}\")" println " -verbose - verbose mode" - println " Used with \e[0;32mnetwork.sh deployCC\e[0m" + println " Used with \033[0;32mnetwork.sh deployCC\033[0m" println " -c - deploy chaincode to channel" println " -ccn - the short name of the chaincode to deploy: basic (default),ledger, private, sbe, secured" println " -ccl - the programming language of the chaincode to deploy: go (default), java, javascript, typescript" @@ -54,10 +54,10 @@ function printHelp() { println " -h - print this message" println println " Possible Mode and flag combinations" - println " \e[0;32mup\e[0m -ca -c -r -d -s -i -verbose" - println " \e[0;32mup createChannel\e[0m -ca -c -r -d -s -i -verbose" - println " \e[0;32mcreateChannel\e[0m -c -r -d -verbose" - println " \e[0;32mdeployCC\e[0m -ccn -ccl -ccv -ccs -ccp -cci -r -d -verbose" + println " \033[0;32mup\033[0m -ca -c -r -d -s -i -verbose" + println " \033[0;32mup createChannel\033[0m -ca -c -r -d -s -i -verbose" + println " \033[0;32mcreateChannel\033[0m -c -r -d -verbose" + println " \033[0;32mdeployCC\033[0m -ccn -ccl -ccv -ccs -ccp -cci -r -d -verbose" println println " Taking all defaults:" println " network.sh up" From a461ff581ee0abc968b393df53918480a2605b95 Mon Sep 17 00:00:00 2001 From: Kestutis Gudynas <44440041+kemi04@users.noreply.github.com> Date: Fri, 18 Sep 2020 12:34:34 +0100 Subject: [PATCH 04/38] Dependencies updated (#336) Signed-off-by: Kestutis Gudynas <44440041+kemi04@users.noreply.github.com> --- chaincode/fabcar/javascript/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chaincode/fabcar/javascript/package.json b/chaincode/fabcar/javascript/package.json index 8403de8e..1adb28b6 100644 --- a/chaincode/fabcar/javascript/package.json +++ b/chaincode/fabcar/javascript/package.json @@ -4,8 +4,8 @@ "description": "FabCar contract implemented in JavaScript", "main": "index.js", "engines": { - "node": ">=8", - "npm": ">=5" + "node": ">=12", + "npm": ">=6.9" }, "scripts": { "lint": "eslint .", From 7f6fa7ce0cb745cb694c0c20916e8e43dfe7415f Mon Sep 17 00:00:00 2001 From: GeonWoo Kim Date: Tue, 22 Sep 2020 18:57:14 +0900 Subject: [PATCH 05/38] Fix Error message application-go (#341) [FAB-18236] * fix Error message * add additional logs to transaction Signed-off-by: KIM GEONWOO --- asset-transfer-basic/application-go/assetTransfer.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/asset-transfer-basic/application-go/assetTransfer.go b/asset-transfer-basic/application-go/assetTransfer.go index 10d40968..b9a100bb 100644 --- a/asset-transfer-basic/application-go/assetTransfer.go +++ b/asset-transfer-basic/application-go/assetTransfer.go @@ -63,12 +63,14 @@ func main() { contract := network.GetContract("basic") + log.Println("--> Submit Transaction: InitLedger, function creates the initial set of assets on the ledger") result, err := contract.SubmitTransaction("InitLedger") if err != nil { - log.Fatalf("failed to evaluate transaction: %v", err) + log.Fatalf("Failed to Submit transaction: %v", err) } log.Println(string(result)) + log.Println("--> Evaluate Transaction: GetAllAssets, function returns all the current assets on the ledger") result, err = contract.EvaluateTransaction("GetAllAssets") if err != nil { log.Fatalf("Failed to evaluate transaction: %v", err) @@ -78,28 +80,28 @@ func main() { log.Println("--> Submit Transaction: CreateAsset, creates new asset with ID, color, owner, size, and appraisedValue arguments") result, err = contract.SubmitTransaction("CreateAsset", "asset13", "yellow", "5", "Tom", "1300") if err != nil { - log.Fatalf("Failed to evaluate transaction: %v", err) + log.Fatalf("Failed to Submit transaction: %v", err) } log.Println(string(result)) log.Println("--> Evaluate Transaction: ReadAsset, function returns an asset with a given assetID") result, err = contract.EvaluateTransaction("ReadAsset", "asset13") if err != nil { - log.Fatalf("failed to evaluate transaction: %v\n", err) + log.Fatalf("Failed to evaluate transaction: %v\n", err) } log.Println(string(result)) log.Println("--> Evaluate Transaction: AssetExists, function returns 'true' if an asset with given assetID exist") result, err = contract.EvaluateTransaction("AssetExists", "asset1") if err != nil { - log.Fatalf("failed to evaluate transaction: %v\n", err) + log.Fatalf("Failed to evaluate transaction: %v\n", err) } log.Println(string(result)) log.Println("--> Submit Transaction: TransferAsset asset1, transfer to new owner of Tom") _, err = contract.SubmitTransaction("TransferAsset", "asset1", "Tom") if err != nil { - log.Fatalf("Failed to submit transaction: %v", err) + log.Fatalf("Failed to Submit transaction: %v", err) } log.Println("--> Evaluate Transaction: ReadAsset, function returns 'asset1' attributes") From 22393b629bcac7f7807cc6998aa44e06ecc77426 Mon Sep 17 00:00:00 2001 From: nikhil550 Date: Fri, 25 Sep 2020 08:09:10 -0400 Subject: [PATCH 06/38] Link to rebased private data tutorial (#342) Signed-off-by: Nikhil Gupta --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5b973748..dcf90721 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ transfer an asset in a more realistic transfer scenario. | -----------|------------------------------|----------|---------|---------| | [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 | | [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://github.com/hyperledger/fabric-samples/tree/master/asset-transfer-private-data/chaincode-go) | 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 | | [Secured agreement](asset-transfer-secured-agreement) | Smart contract that uses implicit private data collections, state-based endorsement, and organization-based ownership and access control to keep data private and securely transfer an asset with the consent of both the current owner and buyer. | [Secured asset transfer](https://hyperledger-fabric.readthedocs.io/en/latest/secured_asset_transfer/secured_private_asset_transfer_tutorial.html) | Go | JavaScript | From 75e3be113726d83a2de6d427608a0fec8e9b9711 Mon Sep 17 00:00:00 2001 From: nikhil550 Date: Sat, 3 Oct 2020 01:53:52 -0400 Subject: [PATCH 07/38] Remove instantiate comment (#343) Signed-off-by: NIKHIL E GUPTA --- test-network/network.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-network/network.sh b/test-network/network.sh index a2190e40..4db35a2c 100755 --- a/test-network/network.sh +++ b/test-network/network.sh @@ -362,7 +362,7 @@ function createChannel() { } -## Call the script to install and instantiate a chaincode on the channel +## Call the script to deploy a chaincode to the channel function deployCC() { scripts/deployCC.sh $CHANNEL_NAME $CC_NAME $CC_SRC_PATH $CC_SRC_LANGUAGE $CC_VERSION $CC_SEQUENCE $CC_INIT_FCN $CC_END_POLICY $CC_COLL_CONFIG $CLI_DELAY $MAX_RETRY $VERBOSE From 327e3a78aa9e97c780e77354275687b6f23e02df Mon Sep 17 00:00:00 2001 From: NIKHIL E GUPTA Date: Sat, 3 Oct 2020 20:54:07 -0400 Subject: [PATCH 08/38] Remove restart mode and add mode specific help text Signed-off-by: NIKHIL E GUPTA --- test-network/network.sh | 56 +-------------- test-network/scriptUtils.sh | 113 +++++++++++++++++++++++++++++++ test-network/scripts/deployCC.sh | 4 -- 3 files changed, 114 insertions(+), 59 deletions(-) diff --git a/test-network/network.sh b/test-network/network.sh index 4db35a2c..4cb19ddc 100755 --- a/test-network/network.sh +++ b/test-network/network.sh @@ -18,57 +18,6 @@ export VERBOSE=false source scriptUtils.sh -# Print the usage message -function printHelp() { - println "Usage: " - println " network.sh [Flags]" - println " Modes:" - println " \033[0;32mup\033[0m - bring up fabric orderer and peer nodes. No channel is created" - println " \033[0;32mup createChannel\033[0m - bring up fabric network with one channel" - println " \033[0;32mcreateChannel\033[0m - create and join a channel after the network is created" - println " \033[0;32mdeployCC\033[0m - deploy the asset transfer basic chaincode on the channel or specify" - println " \033[0;32mdown\033[0m - clear the network with docker-compose down" - println " \033[0;32mrestart\033[0m - restart the network" - println - println " Flags:" - println " Used with \033[0;32mnetwork.sh up\033[0m, \033[0;32mnetwork.sh createChannel\033[0m:" - println " -ca - create Certificate Authorities to generate the crypto material" - println " -c - channel name to use (defaults to \"mychannel\")" - println " -s - the database backend to use: goleveldb (default) or couchdb" - println " -r - CLI times out after certain number of attempts (defaults to 5)" - println " -d - delay duration in seconds (defaults to 3)" - println " -i - the tag to be used to launch the network (defaults to \"latest\")" - println " -cai - the image tag to be used for CA (defaults to \"${CA_IMAGETAG}\")" - println " -verbose - verbose mode" - println " Used with \033[0;32mnetwork.sh deployCC\033[0m" - println " -c - deploy chaincode to channel" - println " -ccn - the short name of the chaincode to deploy: basic (default),ledger, private, sbe, secured" - println " -ccl - the programming language of the chaincode to deploy: go (default), java, javascript, typescript" - println " -ccv - chaincode version. 1.0 (default)" - println " -ccs - chaincode definition sequence. Must be an integer, 1 (default), 2, 3, etc" - println " -ccp - Optional, path to the chaincode. When provided the -ccn will be used as the deployed name and not the short name of the known chaincodes." - println " -ccep - Optional, chaincode endorsement policy, using signature policy syntax. The default policy requires an endorsement from Org1 and Org2" - println " -cccg - Optional, path to a private data collections configuration file" - println " -cci - Optional, chaincode init required function to invoke. When provided this function will be invoked after deployment of the chaincode and will define the chaincode as initialization required." - println - println " -h - print this message" - println - println " Possible Mode and flag combinations" - println " \033[0;32mup\033[0m -ca -c -r -d -s -i -verbose" - println " \033[0;32mup createChannel\033[0m -ca -c -r -d -s -i -verbose" - println " \033[0;32mcreateChannel\033[0m -c -r -d -verbose" - println " \033[0;32mdeployCC\033[0m -ccn -ccl -ccv -ccs -ccp -cci -r -d -verbose" - println - println " Taking all defaults:" - println " network.sh up" - println - println " Examples:" - println " network.sh up createChannel -ca -c mychannel -s couchdb -i 2.0.0" - println " network.sh createChannel -c channelName" - println " network.sh deployCC -ccn basic -ccl javascript" - println " network.sh deployCC -ccn mychaincode -ccp ./user/mychaincode -ccv 1 -ccl javascript" -} - # Obtain CONTAINER_IDS and remove them # TODO Might want to make this optional - could clear other containers # This function is called when you bring a network down @@ -472,7 +421,7 @@ while [[ $# -ge 1 ]] ; do key="$1" case $key in -h ) - printHelp + printHelp $MODE exit 0 ;; -c ) @@ -579,9 +528,6 @@ elif [ "${MODE}" == "deployCC" ]; then deployCC elif [ "${MODE}" == "down" ]; then networkDown -elif [ "${MODE}" == "restart" ]; then - networkDown - networkUp else printHelp exit 1 diff --git a/test-network/scriptUtils.sh b/test-network/scriptUtils.sh index 3b46a1a8..043d33f0 100755 --- a/test-network/scriptUtils.sh +++ b/test-network/scriptUtils.sh @@ -6,6 +6,119 @@ C_GREEN='\033[0;32m' C_BLUE='\033[0;34m' C_YELLOW='\033[1;33m' +# Print the usage message +function printHelp() { + USAGE="$1" + if [ "$USAGE" == "up" ]; then + println "Usage: " + println " network.sh \033[0;32mup\033[0m [Flags]" + println + println " Flags:" + println " -ca - Use Certificate Authorities to generate network crypto material" + println " -c - Name of channel to create (defaults to \"mychannel\")" + println " -s - Peer state database to deploy: goleveldb (default) or couchdb" + println " -r - CLI times out after certain number of attempts (defaults to 5)" + println " -d - CLI delays for a certain number of seconds (defaults to 3)" + println " -i - Docker image tag of Fabric to deploy (defaults to \"latest\")" + println " -cai - Docker image tag of Fabric CA to deploy (defaults to \"${CA_IMAGETAG}\")" + println " -verbose - Verbose mode" + println + println " -h - Print this message" + println + println " Possible Mode and flag combinations" + println " \033[0;32mup\033[0m -ca -r -d -s -i -cai -verbose" + println " \033[0;32mup createChannel\033[0m -ca -c -r -d -s -i -cai -verbose" + println + println " Examples:" + println " network.sh up createChannel -ca -c mychannel -s couchdb -i 2.0.0" + elif [ "$USAGE" == "createChannel" ]; then + println "Usage: " + println " network.sh \033[0;32mcreateChannel\033[0m [Flags]" + println + println " Flags:" + println " -c - Name of channel to create (defaults to \"mychannel\")" + println " -r - CLI times out after certain number of attempts (defaults to 5)" + println " -d - CLI delays for a certain number of seconds (defaults to 3)" + println " -verbose - Verbose mode" + println + println " -h - Print this message" + println + println " Possible Mode and flag combinations" + println " \033[0;32mcreateChannel\033[0m -c -r -d -verbose" + println + println " Examples:" + println " network.sh createChannel -c channelName" + elif [ "$USAGE" == "deployCC" ]; then + println "Usage: " + println " network.sh \033[0;32mdeployCC\033[0m [Flags]" + println + println " Flags:" + println " -c - Name of channel to deploy chaincode to" + println " -ccn - Chaincode name. This flag can be used to deploy one of the asset transfer samples to a channel. Sample options: basic (default),ledger, private, sbe, secured" + println " -ccl - Programming language of chaincode to deploy: go (default), java, javascript, typescript" + println " -ccv - Chaincode version. 1.0 (default), v2, version3.x, etc" + println " -ccs - Chaincode definition sequence. Must be an integer, 1 (default), 2, 3, etc" + println " -ccp - (Optional) File path to the chaincode. When provided, the -ccn flag will be used only for the chaincode name." + println " -ccep - (Optional) Chaincode endorsement policy using signature policy syntax. The default policy requires an endorsement from Org1 and Org2" + println " -cccg - (Optional) File path to private data collections configuration file" + println " -cci - (Optional) Name of chaincode initialization function. When a function is provided, the execution of init will be requested and the function will be invoked." + println + println " -h - Print this message" + println + println " Possible Mode and flag combinations" + println " \033[0;32mdeployCC\033[0m -ccn -ccl -ccv -ccs -ccp -cci -r -d -verbose" + println + println " Examples:" + println " network.sh deployCC -ccn basic -ccl javascript" + println " network.sh deployCC -ccn mychaincode -ccp ./user/mychaincode -ccv 1 -ccl javascript" + else + println "Usage: " + println " network.sh [Flags]" + println " Modes:" + println " \033[0;32mup\033[0m - Bring up Fabric orderer and peer nodes. No channel is created" + println " \033[0;32mup createChannel\033[0m - Bring up fabric network with one channel" + println " \033[0;32mcreateChannel\033[0m - Create and join a channel after the network is created" + println " \033[0;32mdeployCC\033[0m - Deploy a chaincode to a channel (defaults to asset-transfer-basic)" + println " \033[0;32mdown\033[0m - Bring down the network" + println + println " Flags:" + println " Used with \033[0;32mnetwork.sh up\033[0m, \033[0;32mnetwork.sh createChannel\033[0m:" + println " -ca - Use Certificate Authorities to generate network crypto material" + println " -c - Name of channel to create (defaults to \"mychannel\")" + println " -s - Peer state database to deploy: goleveldb (default) or couchdb" + println " -r - CLI times out after certain number of attempts (defaults to 5)" + println " -d - CLI delays for a certain number of seconds (defaults to 3)" + println " -i - Docker image tag of Fabric to deploy (defaults to \"latest\")" + println " -cai - Docker image tag of Fabric CA to deploy (defaults to \"${CA_IMAGETAG}\")" + println " -verbose - Verbose mode" + println + println " Used with \033[0;32mnetwork.sh deployCC\033[0m" + println " -c - Name of channel to deploy chaincode to" + println " -ccn - Chaincode name. This flag can be used to deploy one of the asset transfer samples to a channel. Sample options: basic (default),ledger, private, sbe, secured" + println " -ccl - Programming language of the chaincode to deploy: go (default), java, javascript, typescript" + println " -ccv - Chaincode version. 1.0 (default), v2, version3.x, etc" + println " -ccs - Chaincode definition sequence. Must be an integer, 1 (default), 2, 3, etc" + println " -ccp - (Optional) File path to the chaincode. When provided, the -ccn flag will be used only for the chaincode name." + println " -ccep - (Optional) Chaincode endorsement policy using signature policy syntax. The default policy requires an endorsement from Org1 and Org2" + println " -cccg - (Optional) File path to private data collections configuration file" + println " -cci - (Optional) Name of chaincode initialization function. When a function is provided, the execution of init will be requested and the function will be invoked." + println + println " -h - Print this message" + println + println " Possible Mode and flag combinations" + println " \033[0;32mup\033[0m -ca -r -d -s -i -cai -verbose" + println " \033[0;32mup createChannel\033[0m -ca -c -r -d -s -i -cai -verbose" + println " \033[0;32mcreateChannel\033[0m -c -r -d -verbose" + println " \033[0;32mdeployCC\033[0m -ccn -ccl -ccv -ccs -ccp -cci -r -d -verbose" + println + println " Examples:" + println " network.sh up createChannel -ca -c mychannel -s couchdb -i 2.0.0" + println " network.sh createChannel -c channelName" + println " network.sh deployCC -ccn basic -ccl javascript" + println " network.sh deployCC -ccn mychaincode -ccp ./user/mychaincode -ccv 1 -ccl javascript" + fi +} + # println echos string function println() { echo -e "$1" diff --git a/test-network/scripts/deployCC.sh b/test-network/scripts/deployCC.sh index 1b75ce62..2125a794 100755 --- a/test-network/scripts/deployCC.sh +++ b/test-network/scripts/deployCC.sh @@ -134,10 +134,6 @@ else CC_COLL_CONFIG="--collections-config $CC_COLL_CONFIG" fi -#if [ "$CC_INIT_FCN" = "NA" ]; then -# INIT_REQUIRED="" -#fi - # import utils . scripts/envVar.sh From 6d196bd9df3ebcc51c690b2fb7c118768c01ccef Mon Sep 17 00:00:00 2001 From: NIKHIL E GUPTA Date: Thu, 17 Sep 2020 12:45:54 -0400 Subject: [PATCH 09/38] Move off chain data samples to basic chaincode Signed-off-by: NIKHIL E GUPTA --- off_chain_data/README.md | 150 +++++++----------- off_chain_data/addAssets.js | 118 ++++++++++++++ off_chain_data/addMarbles.js | 117 -------------- .../{deleteMarble.js => deleteAsset.js} | 15 +- off_chain_data/network-clean.sh | 2 +- off_chain_data/package.json | 4 +- off_chain_data/startFabric.sh | 150 +----------------- .../{transferMarble.js => transferAsset.js} | 15 +- 8 files changed, 189 insertions(+), 382 deletions(-) create mode 100644 off_chain_data/addAssets.js delete mode 100644 off_chain_data/addMarbles.js rename off_chain_data/{deleteMarble.js => deleteAsset.js} (78%) rename off_chain_data/{transferMarble.js => transferAsset.js} (75%) diff --git a/off_chain_data/README.md b/off_chain_data/README.md index 45eacd1a..156649bf 100644 --- a/off_chain_data/README.md +++ b/off_chain_data/README.md @@ -10,9 +10,9 @@ CouchDB. ## Getting started -This sample uses Node Fabric SDK application code similar to the `fabcar` sample -to connect to a running instance of the Fabric test network. Make sure that you -are running the following commands from the `off_chain_data` directory. +This sample uses Node Fabric SDK application code to connect to a running instance +of the Fabric test network. Make sure that you are running the following +commands from the `off_chain_data` directory. ### Starting the Network @@ -25,7 +25,8 @@ Use the following command to start the sample network: This command will deploy an instance of the Fabric test network. The network consists of an ordering service, two peer organizations with one peers each, and a CA for each org. The command also creates a channel named `mychannel`. The -marbles chaincode will be installed on both peers and deployed to the channel. +`asset-transfer-basic` chaincode will be installed on both peers and deployed to +the channel. ### Configuration @@ -55,10 +56,11 @@ If you set the "use_couchdb" option to true in `config.json`, you can run the following command start a local instance of CouchDB using docker: ``` -docker run --publish 5990:5984 --detach --name offchaindb couchdb +docker run --publish 5990:5984 --detach --name offchaindb couchdb:2.3.1 docker start offchaindb ``` + ### Install dependencies You need to install Node.js version 8.9.x to use the sample application code. @@ -92,53 +94,7 @@ node blockEventListener.js If the command is successful, you should see the output of the listener reading the configuration blocks of `mychannel` in addition to the blocks that recorded -the approval and commitment of the marbles chaincode definition. - -``` -Listening for block events, nextblock: 0 -Added block 0 to ProcessingMap -Added block 1 to ProcessingMap -Added block 2 to ProcessingMap -Added block 3 to ProcessingMap -Added block 4 to ProcessingMap -Added block 5 to ProcessingMap -Added block 6 to ProcessingMap ------------------------------------------------- -Block Number: 0 ------------------------------------------------- -Block Number: 1 ------------------------------------------------- -Block Number: 2 ------------------------------------------------- -Block Number: 3 -Block Timestamp: 2019-08-08T19:47:56.148Z -ChaincodeID: _lifecycle -[] ------------------------------------------------- -Block Number: 4 -Block Timestamp: 2019-08-08T19:48:00.234Z -ChaincodeID: _lifecycle -[] ------------------------------------------------- -Block Number: 5 -Block Timestamp: 2019-08-08T19:48:14.092Z -ChaincodeID: _lifecycle -[ { key: 'namespaces/fields/marbles/Collections', - is_delete: false, - value: '\u0012\u0000' }, - { key: 'namespaces/fields/marbles/EndorsementInfo', - is_delete: false, - value: '\u0012\r\n\u00031.0\u0010\u0001\u001a\u0004escc' }, - { key: 'namespaces/fields/marbles/Sequence', - is_delete: false, - value: '\b\u0001' }, - { key: 'namespaces/fields/marbles/ValidationInfo', - is_delete: false, - value: '\u00122\n\u0004vscc\u0012*\n(\u0012\f\u0012\n\b\u0002\u0012\u0002\b\u0000\u0012\u0002\b\u0001\u001a\u000b\u0012\t\n\u0007Org1MSP\u001a\u000b\u0012\t\n\u0007Org2MSP' }, - { key: 'namespaces/metadata/marbles', - is_delete: false, - value: '\n\u0013ChaincodeDefinition\u0012\bSequence\u0012\u000fEndorsementInfo\u0012\u000eValidationInfo\u0012\u000bCollections' } ] -``` +the approval and commitment of the assets chaincode definition. `blockEventListener.js` creates a listener named "offchain-listener" on the channel `mychannel`. The listener writes each block added to the channel to a @@ -157,7 +113,7 @@ read-write set. The channel event listener also writes metadata from each block to a log file defined as channelid_chaincodeid.log. In this example, events will be written to -a file named `mychannel_marbles.log`. This allows you to record a history of +a file named `mychannel_basic.log`. This allows you to record a history of changes made by each block for each key in addition to storing the latest value of the world state. @@ -166,42 +122,42 @@ new window to execute the next parts of the demo. ### Generate data on the blockchain -Now that our listener is setup, we can generate data using the marbles chaincode +Now that our listener is setup, we can generate data using the assets chaincode and use our application to replicate the data to our database. Open a new terminal and navigate to the `fabric-samples/off_chain_data` directory. -You can use the `addMarbles.js` file to add random sample data to blockchain. -The file uses the configuration information stored in `addMarbles.json` to -create a series of marbles. This file will be created during the first execution -of `addMarbles.js` if it does not exist. This program can be run multiple times -without changing the properties. The `nextMarbleNumber` will be incremented and -stored in the `addMarbles.json` file. +You can use the `addAssets.js` file to add random sample data to blockchain. +The file uses the configuration information stored in `addAssets.json` to +create a series of assets. This file will be created during the first execution +of `addAssets.js` if it does not exist. This program can be run multiple times +without changing the properties. The `nextAssetNumber` will be incremented and +stored in the `addAssets.json` file. ``` { - "nextMarbleNumber": 100, - "numberMarblesToAdd": 20 + "nextAssetNumber": 100, + "numberAssetsToAdd": 20 } ``` -Open a new window and run the following command to add 20 marbles to the +Open a new window and run the following command to add 20 assets to the blockchain: ``` -node addMarbles.js +node addAssets.js ``` -After the marbles have been added to the ledger, use the following command to -transfer one of the marbles to a new owner: +After the assets have been added to the ledger, use the following command to +transfer one of the assets to a new owner: ``` -node transferMarble.js marble110 james +node transferAsset.js asset110 james ``` -Now run the following command to delete the marble that was transferred: +Now run the following command to delete the asset that was transferred: ``` -node deleteMarble.js marble110 +node deleteAsset.js asset110 ``` ## Offchain CouchDB storage: @@ -215,17 +171,17 @@ The first table is an offline representation of the current world state of the blockchain ledger. This table was created using the read-write set data from the blocks. If the listener is running, this table should be the same as the latest values in the state database running on your peer. The table is named -after the channelid and chaincodeid, and is named mychannel_marbles in this +after the channelid and chaincodeid, and is named mychannel_basic in this example. You can navigate to this table using your browser: -http://127.0.0.1:5990/mychannel_marbles/_all_docs +http://127.0.0.1:5990/mychannel_basic/_all_docs A second table records each block as a historical record entry, and was created using the block data that was recorded in the log file. The table name appends -history to the name of the first table, and is named mychannel_marbles_history +history to the name of the first table, and is named mychannel_basic_history in this example. You can also navigate to this table using your browser: -http://127.0.0.1:5990/mychannel_marbles_history/_all_docs +http://127.0.0.1:5990/mychannel_basic_history/_all_docs -### Configure a map/reduce view for summarizing counts of marbles by color: +### Configure a map/reduce view for summarizing counts of assets by color: Now that we have state and history data replicated to tables in CouchDB, we can use the following commands query our off-chain data. We will also add an @@ -236,16 +192,16 @@ created when events are received. Open a new terminal window and execute the following: ``` -curl -X PUT http://127.0.0.1:5990/mychannel_marbles/_design/colorviewdesign -d '{"views":{"colorview":{"map":"function (doc) { emit(doc.color, 1);}","reduce":"function ( keys , values , combine ) {return sum( values )}"}}}' -H 'Content-Type:application/json' +curl -X PUT http://127.0.0.1:5990/mychannel_basic/_design/colorviewdesign -d '{"views":{"colorview":{"map":"function (doc) { emit(doc.color, 1);}","reduce":"function ( keys , values , combine ) {return sum( values )}"}}}' -H 'Content-Type:application/json' ``` -Execute a query to retrieve the total number of marbles (reduce function): +Execute a query to retrieve the total number of assets (reduce function): ``` -curl -X GET http://127.0.0.1:5990/mychannel_marbles/_design/colorviewdesign/_view/colorview?reduce=true +curl -X GET http://127.0.0.1:5990/mychannel_basic/_design/colorviewdesign/_view/colorview?reduce=true ``` -If successful, this command will return the number of marbles in the blockchain +If successful, this command will return the number of assets in the blockchain world state, without having to query the blockchain ledger: ``` @@ -254,13 +210,13 @@ world state, without having to query the blockchain ledger: ]} ``` -Execute a new query to retrieve the number of marbles by color (map function): +Execute a new query to retrieve the number of assets by color (map function): ``` -curl -X GET http://127.0.0.1:5990/mychannel_marbles/_design/colorviewdesign/_view/colorview?group=true +curl -X GET http://127.0.0.1:5990/mychannel_basic/_design/colorviewdesign/_view/colorview?group=true ``` -The command will return a list of marbles by color from the CouchDB database. +The command will return a list of assets by color from the CouchDB database. ``` {"rows":[ @@ -275,27 +231,27 @@ The command will return a list of marbles by color from the CouchDB database. To run a more complex command that reads through the block history database, we will create an index of the blocknumber, sequence, and key fields. This index -will support a query that traces the history of each marble. Execute the +will support a query that traces the history of each asset. Execute the following command to create the index: ``` -curl -X POST http://127.0.0.1:5990/mychannel_marbles_history/_index -d '{"index":{"fields":["blocknumber", "sequence", "key"]},"name":"marble_history"}' -H 'Content-Type:application/json' +curl -X POST http://127.0.0.1:5990/mychannel_basic_history/_index -d '{"index":{"fields":["blocknumber", "sequence", "key"]},"name":"asset_history"}' -H 'Content-Type:application/json' ``` -Now execute a query to retrieve the history for the marble we transferred and +Now execute a query to retrieve the history for the asset we transferred and then deleted: ``` -curl -X POST http://127.0.0.1:5990/mychannel_marbles_history/_find -d '{"selector":{"key":{"$eq":"marble110"}}, "fields":["blocknumber","is_delete","value"],"sort":[{"blocknumber":"asc"}, {"sequence":"asc"}]}' -H 'Content-Type:application/json' +curl -X POST http://127.0.0.1:5990/mychannel_basic_history/_find -d '{"selector":{"key":{"$eq":"asset110"}}, "fields":["blocknumber","is_delete","value"],"sort":[{"blocknumber":"asc"}, {"sequence":"asc"}]}' -H 'Content-Type:application/json' ``` -You should see the transaction history of the marble that was created, +You should see the transaction history of the asset that was created, transferred, and then removed from the ledger. ``` {"docs":[ -{"blocknumber":12,"is_delete":false,"value":"{\"docType\":\"marble\",\"name\":\"marble110\",\"color\":\"blue\",\"size\":60,\"owner\":\"debra\"}"}, -{"blocknumber":22,"is_delete":false,"value":"{\"docType\":\"marble\",\"name\":\"marble110\",\"color\":\"blue\",\"size\":60,\"owner\":\"james\"}"}, +{"blocknumber":12,"is_delete":false,"value":"{\"docType\":\"asset\",\"name\":\"asset110\",\"color\":\"blue\",\"size\":60,\"owner\":\"debra\"}"}, +{"blocknumber":22,"is_delete":false,"value":"{\"docType\":\"asset\",\"name\":\"asset110\",\"color\":\"blue\",\"size\":60,\"owner\":\"james\"}"}, {"blocknumber":23,"is_delete":true,"value":""} ]} ``` @@ -309,19 +265,19 @@ have missed. If you ran through the example steps above, navigate back to the terminal window where `blockEventListener.js` is running and close it. Once the listener is no -longer running, use the following command to add 20 more marbles to the +longer running, use the following command to add 20 more assets to the ledger: ``` -node addMarbles.js +node addAssets.js ``` -The listener will not be able to add the new marbles to your CouchDB database. +The listener will not be able to add the new assets to your CouchDB database. If you check the current state table using the reduce command, you will only -be able to see the original marbles in your database. +be able to see the original assets in your database. ``` -curl -X GET http://127.0.0.1:5990/mychannel_marbles/_design/colorviewdesign/_view/colorview?reduce=true +curl -X GET http://127.0.0.1:5990/mychannel_basic/_design/colorviewdesign/_view/colorview?reduce=true ``` To add the new data to your off-chain database, remove the `nextblock.txt` @@ -337,15 +293,15 @@ You can new re-run the channel listener to read every block from the channel: node blockEventListener.js ``` -This will rebuild the CouchDB tables and include the 20 marbles that have been +This will rebuild the CouchDB tables and include the 20 assets that have been added to the ledger. If you run the reduce command against your database one more time, ``` -curl -X GET http://127.0.0.1:5990/mychannel_marbles/_design/colorviewdesign/_view/colorview?reduce=true +curl -X GET http://127.0.0.1:5990/mychannel_basic/_design/colorviewdesign/_view/colorview?reduce=true ``` -you will be able to see that all of the marbles have been added to your +you will be able to see that all of the assets have been added to your database: ``` @@ -369,4 +325,4 @@ Running the script will complete the following actions: * Remove the certificates you generated by deleting the `wallet` folder. * Delete `nextblock.txt` so you can start with the first block next time you operate the listener. -* Removes `addMarbles.json`. +* Removes `addAssets.json`. diff --git a/off_chain_data/addAssets.js b/off_chain_data/addAssets.js new file mode 100644 index 00000000..1137d5f7 --- /dev/null +++ b/off_chain_data/addAssets.js @@ -0,0 +1,118 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * + */ + +/* + * + * addAssets.js will add random sample data to blockchain. + * + * $ node addAssets.js + * + * addAssets will add 10 Assets by default with a starting Asset name of "Asset100". + * Additional Assets will be added by incrementing the number at the end of the Asset name. + * + * The properties for adding Assets are stored in addAssets.json. This file will be created + * during the first execution of the utility if it does not exist. The utility can be run + * multiple times without changing the properties. The nextAssetNumber will be incremented and + * stored in the JSON file. + * + * { + * "nextAssetNumber": 100, + * "numberAssetsToAdd": 10 + * } + * + */ + +'use strict'; + +const { Wallets, Gateway } = require('fabric-network'); +const fs = require('fs'); +const path = require('path'); + +const addAssetsConfigFile = path.resolve(__dirname, 'addAssets.json'); + +const colors=[ 'blue', 'red', 'yellow', 'green', 'white', 'purple' ]; +const owners=[ 'tom', 'fred', 'julie', 'james', 'janet', 'henry', 'alice', 'marie', 'sam', 'debra', 'nancy']; +const sizes=[ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 ]; +const appraisedValues=[ 300, 310, 320, 330, 340, 350, 360, 370, 380, 390 ]; +const docType='asset' + +const config = require('./config.json'); +const channelid = config.channelid; + +async function main() { + + try { + + let nextAssetNumber; + let numberAssetsToAdd; + let addAssetsConfig; + + // check to see if there is a config json defined + if (fs.existsSync(addAssetsConfigFile)) { + // read file the next asset and number of assets to create + let addAssetsConfigJSON = fs.readFileSync(addAssetsConfigFile, 'utf8'); + addAssetsConfig = JSON.parse(addAssetsConfigJSON); + nextAssetNumber = addAssetsConfig.nextAssetNumber; + numberAssetsToAdd = addAssetsConfig.numberAssetsToAdd; + } else { + nextAssetNumber = 100; + numberAssetsToAdd = 20; + // create a default config and save + addAssetsConfig = new Object; + addAssetsConfig.nextAssetNumber = nextAssetNumber; + addAssetsConfig.numberAssetsToAdd = numberAssetsToAdd; + fs.writeFileSync(addAssetsConfigFile, JSON.stringify(addAssetsConfig, null, 2)); + } + + // Parse the connection profile. + const ccpPath = path.resolve(__dirname, '..', 'test-network','organizations','peerOrganizations','org1.example.com', 'connection-org1.json'); + const ccp = JSON.parse(fs.readFileSync(ccpPath, 'utf8')); + + // Configure a wallet. This wallet must already be primed with an identity that + // the application can use to interact with the peer node. + const walletPath = path.resolve(__dirname, 'wallet'); + const wallet = await Wallets.newFileSystemWallet(walletPath); + + // Create a new gateway, and connect to the gateway peer node(s). The identity + // specified must already exist in the specified wallet. + const gateway = new Gateway(); + await gateway.connect(ccp, { wallet, identity: 'appUser', discovery: { enabled: true, asLocalhost: true } }); + + // Get the network channel that the smart contract is deployed to. + const network = await gateway.getNetwork(channelid); + + // Get the smart contract from the network channel. + const contract = network.getContract('basic'); + + for (var counter = nextAssetNumber; counter < nextAssetNumber + numberAssetsToAdd; counter++) { + + var randomColor = Math.floor(Math.random() * (6)); + var randomOwner = Math.floor(Math.random() * (11)); + var randomSize = Math.floor(Math.random() * (10)); + var randomValue = Math.floor(Math.random() * (9)); + + // Submit the 'CreateAsset' transaction to the smart contract, and wait for it + // to be committed to the ledger. + await contract.submitTransaction('CreateAsset', docType+counter, colors[randomColor], ''+sizes[randomSize], owners[randomOwner],appraisedValues[randomValue]); + console.log("Adding asset: " + docType + counter + " owner:" + owners[randomOwner] + " color:" + colors[randomColor] + " size:" + '' + sizes[randomSize] + " appraised value:" + '' + appraisedValues[randomValue] ); + + } + + await gateway.disconnect(); + + addAssetsConfig.nextAssetNumber = nextAssetNumber + numberAssetsToAdd; + + fs.writeFileSync(addAssetsConfigFile, JSON.stringify(addAssetsConfig, null, 2)); + + } catch (error) { + console.error(`Failed to submit transaction: ${error}`); + process.exit(1); + } + +} + +main(); diff --git a/off_chain_data/addMarbles.js b/off_chain_data/addMarbles.js deleted file mode 100644 index 8fdbdffc..00000000 --- a/off_chain_data/addMarbles.js +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright IBM Corp. All Rights Reserved. - * - * SPDX-License-Identifier: Apache-2.0 - * - */ - -/* - * - * addMarbles.js will add random sample data to blockchain. - * - * $ node addMarbles.js - * - * addMarbles will add 10 marbles by default with a starting marble name of "marble100". - * Additional marbles will be added by incrementing the number at the end of the marble name. - * - * The properties for adding marbles are stored in addMarbles.json. This file will be created - * during the first execution of the utility if it does not exist. The utility can be run - * multiple times without changing the properties. The nextMarbleNumber will be incremented and - * stored in the JSON file. - * - * { - * "nextMarbleNumber": 100, - * "numberMarblesToAdd": 10 - * } - * - */ - -'use strict'; - -const { Wallets, Gateway } = require('fabric-network'); -const fs = require('fs'); -const path = require('path'); - -const addMarblesConfigFile = path.resolve(__dirname, 'addMarbles.json'); - -const colors=[ 'blue', 'red', 'yellow', 'green', 'white', 'purple' ]; -const owners=[ 'tom', 'fred', 'julie', 'james', 'janet', 'henry', 'alice', 'marie', 'sam', 'debra', 'nancy']; -const sizes=[ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 ]; -const docType='marble' - -const config = require('./config.json'); -const channelid = config.channelid; - -async function main() { - - try { - - let nextMarbleNumber; - let numberMarblesToAdd; - let addMarblesConfig; - - // check to see if there is a config json defined - if (fs.existsSync(addMarblesConfigFile)) { - // read file the next marble and number of marbles to create - let addMarblesConfigJSON = fs.readFileSync(addMarblesConfigFile, 'utf8'); - addMarblesConfig = JSON.parse(addMarblesConfigJSON); - nextMarbleNumber = addMarblesConfig.nextMarbleNumber; - numberMarblesToAdd = addMarblesConfig.numberMarblesToAdd; - } else { - nextMarbleNumber = 100; - numberMarblesToAdd = 20; - // create a default config and save - addMarblesConfig = new Object; - addMarblesConfig.nextMarbleNumber = nextMarbleNumber; - addMarblesConfig.numberMarblesToAdd = numberMarblesToAdd; - fs.writeFileSync(addMarblesConfigFile, JSON.stringify(addMarblesConfig, null, 2)); - } - - // Parse the connection profile. This would be the path to the file downloaded - // from the IBM Blockchain Platform operational console. - const ccpPath = path.resolve(__dirname, '..', 'test-network','organizations','peerOrganizations','org1.example.com', 'connection-org1.json'); - const ccp = JSON.parse(fs.readFileSync(ccpPath, 'utf8')); - - // Configure a wallet. This wallet must already be primed with an identity that - // the application can use to interact with the peer node. - const walletPath = path.resolve(__dirname, 'wallet'); - const wallet = await Wallets.newFileSystemWallet(walletPath); - - // Create a new gateway, and connect to the gateway peer node(s). The identity - // specified must already exist in the specified wallet. - const gateway = new Gateway(); - await gateway.connect(ccp, { wallet, identity: 'appUser', discovery: { enabled: true, asLocalhost: true } }); - - // Get the network channel that the smart contract is deployed to. - const network = await gateway.getNetwork(channelid); - - // Get the smart contract from the network channel. - const contract = network.getContract('marbles'); - - for (var counter = nextMarbleNumber; counter < nextMarbleNumber + numberMarblesToAdd; counter++) { - - var randomColor = Math.floor(Math.random() * (6)); - var randomOwner = Math.floor(Math.random() * (11)); - var randomSize = Math.floor(Math.random() * (10)); - - // Submit the 'initMarble' transaction to the smart contract, and wait for it - // to be committed to the ledger. - await contract.submitTransaction('initMarble', docType+counter, colors[randomColor], ''+sizes[randomSize], owners[randomOwner]); - console.log("Adding marble: " + docType + counter + " owner:" + owners[randomOwner] + " color:" + colors[randomColor] + " size:" + '' + sizes[randomSize] ); - - } - - await gateway.disconnect(); - - addMarblesConfig.nextMarbleNumber = nextMarbleNumber + numberMarblesToAdd; - - fs.writeFileSync(addMarblesConfigFile, JSON.stringify(addMarblesConfig, null, 2)); - - } catch (error) { - console.error(`Failed to submit transaction: ${error}`); - process.exit(1); - } - -} - -main(); diff --git a/off_chain_data/deleteMarble.js b/off_chain_data/deleteAsset.js similarity index 78% rename from off_chain_data/deleteMarble.js rename to off_chain_data/deleteAsset.js index b12706c0..aeede815 100644 --- a/off_chain_data/deleteMarble.js +++ b/off_chain_data/deleteAsset.js @@ -7,9 +7,9 @@ /* * - * deleteMarble.js will delete a specified marble. Example: + * deleteAsset.js will delete a specified asset. Example: * - * $ node deleteMarble.js marble100 + * $ node deleteAsset.js asset100 * * The utility is meant to demonstrate delete block events. */ @@ -26,7 +26,7 @@ const channelid = config.channelid; async function main() { if (process.argv[2] == undefined) { - console.log("Usage: node deleteMarble marbleId"); + console.log("Usage: node deleteAsset AssetId"); process.exit(1); } @@ -34,8 +34,7 @@ async function main() { try { - // Parse the connection profile. This would be the path to the file downloaded - // from the IBM Blockchain Platform operational console. + // Parse the connection profile. const ccpPath = path.resolve(__dirname, '..', 'test-network','organizations','peerOrganizations','org1.example.com', 'connection-org1.json'); const ccp = JSON.parse(fs.readFileSync(ccpPath, 'utf8')); @@ -53,10 +52,10 @@ async function main() { const network = await gateway.getNetwork(channelid); // Get the smart contract from the network channel. - const contract = network.getContract('marbles'); + const contract = network.getContract('basic'); - await contract.submitTransaction('delete', deletekey); - console.log("Deleted marble: " + deletekey); + await contract.submitTransaction('DeleteAsset', deletekey); + console.log("Deleted asset: " + deletekey); await gateway.disconnect(); diff --git a/off_chain_data/network-clean.sh b/off_chain_data/network-clean.sh index 14dc7098..1d24a2a7 100755 --- a/off_chain_data/network-clean.sh +++ b/off_chain_data/network-clean.sh @@ -14,7 +14,7 @@ popd # clean out any old identites in the wallets rm -rf wallet -rm -rf addMarbles.json mychannel_marbles.log mychannel__lifecycle.log nextblock.txt +rm -rf addAssets.json mychannel_basic.log mychannel__lifecycle.log nextblock.txt docker stop offchaindb docker rm offchaindb diff --git a/off_chain_data/package.json b/off_chain_data/package.json index 9bb41be9..a693870e 100644 --- a/off_chain_data/package.json +++ b/off_chain_data/package.json @@ -15,8 +15,8 @@ "author": "Hyperledger", "license": "Apache-2.0", "dependencies": { - "fabric-ca-client": "^2.1.0", - "fabric-network": "^2.1.0" + "fabric-ca-client": "^2.2.0", + "fabric-network": "^2.2.0" }, "devDependencies": { "chai": "^4.2.0", diff --git a/off_chain_data/startFabric.sh b/off_chain_data/startFabric.sh index 1f54e6ea..fa2dedcf 100755 --- a/off_chain_data/startFabric.sh +++ b/off_chain_data/startFabric.sh @@ -7,161 +7,13 @@ # Exit on first error set -e pipefail -# don't rewrite paths for Windows Git Bash users -export MSYS_NO_PATHCONV=1 starttime=$(date +%s) -CC_SRC_LANGUAGE=golang -CC_RUNTIME_LANGUAGE=golang -CC_SRC_PATH=../chaincode/marbles02/go - -echo Vendoring Go dependencies ... -pushd ../chaincode/marbles02/go -GO111MODULE=on go mod vendor -popd -echo Finished vendoring Go dependencies # launch network; create channel and join peer to channel pushd ../test-network ./network.sh down ./network.sh up createChannel -ca -s couchdb - -export PATH=${PWD}/../bin:${PWD}:$PATH -export FABRIC_CFG_PATH=${PWD}/../config - -# import environment variables -. scripts/envVar.sh - -echo "Packaging the marbles smart contract" - -setGlobals 1 - -peer lifecycle chaincode package marbles.tar.gz \ - --path $CC_SRC_PATH \ - --lang $CC_RUNTIME_LANGUAGE \ - --label marblesv1 - -echo "Installing smart contract on peer0.org1.example.com" - -peer lifecycle chaincode install marbles.tar.gz - - -echo "Installing smart contract on peer0.org2.example.com" - -setGlobals 2 - -peer lifecycle chaincode install marbles.tar.gz - - -echo "Query the chaincode package id" - -setGlobals 1 - -peer lifecycle chaincode queryinstalled >&log.txt - -PACKAGE_ID=$(sed -n "/marblesv1/{s/^Package ID: //; s/, Label:.*$//; p;}" log.txt) - -echo "Approving the chaincode definition for org1.example.com" - -peer lifecycle chaincode approveformyorg \ - -o localhost:7050 \ - --ordererTLSHostnameOverride orderer.example.com \ - --channelID mychannel \ - --name marbles \ - --version 1.0 \ - --init-required \ - --signature-policy AND"('Org1MSP.member','Org2MSP.member')" \ - --sequence 1 \ - --package-id $PACKAGE_ID \ - --tls \ - --cafile ${ORDERER_CA} - -echo "Approving the chaincode definition for org2.example.com" - -setGlobals 2 - -peer lifecycle chaincode approveformyorg \ - -o localhost:7050 \ - --ordererTLSHostnameOverride orderer.example.com \ - --channelID mychannel \ - --name marbles \ - --version 1.0 \ - --init-required \ - --signature-policy AND"('Org1MSP.member','Org2MSP.member')" \ - --sequence 1 \ - --package-id $PACKAGE_ID \ - --tls \ - --cafile ${ORDERER_CA} - -echo "Checking if the chaincode definition is ready to commit" - -peer lifecycle chaincode checkcommitreadiness \ - --channelID mychannel \ - --name marbles \ - --version 1.0 \ - --sequence 1 \ - --output json \ - --init-required \ - --signature-policy AND"('Org1MSP.member','Org2MSP.member')" >&log.txt - -rc=0 -for var in "\"Org1MSP\": true" "\"Org2MSP\": true" -do - grep "$var" log.txt &>/dev/null || let rc=1 -done - -if test $rc -eq 0; then - echo "Chaincode definition is ready to commit" -else - sleep 10 -fi - -parsePeerConnectionParameters 1 2 - -echo "Commit the chaincode definition to the channel" - -peer lifecycle chaincode commit \ - -o localhost:7050 \ - --ordererTLSHostnameOverride orderer.example.com \ - --channelID mychannel \ - --name marbles \ - --version 1.0 \ - --init-required \ - --signature-policy AND"('Org1MSP.member','Org2MSP.member')" \ - --sequence 1 \ - --tls \ - --cafile ${ORDERER_CA} \ - $PEER_CONN_PARMS - -echo "Check if the chaincode has been committed to the channel ..." - -peer lifecycle chaincode querycommitted \ - --channelID mychannel \ - --name marbles >&log.txt - -EXPECTED_RESULT="Version: 1.0, Sequence: 1, Endorsement Plugin: escc, Validation Plugin: vscc" -VALUE=$(grep -o "Version: 1.0, Sequence: 1, Endorsement Plugin: escc, Validation Plugin: vscc" log.txt) -echo "$VALUE" - -if [ "$VALUE" = "Version: 1.0, Sequence: 1, Endorsement Plugin: escc, Validation Plugin: vscc" ] ; then - echo "chaincode has been committed" -else - sleep 10 -fi - -echo "invoke the marbles chaincode init function ... " - -peer chaincode invoke \ - -o localhost:7050 \ - --ordererTLSHostnameOverride orderer.example.com \ - -C mychannel \ - -n marbles \ - --isInit \ - -c '{"Args":["Init"]}' \ - --tls \ - --cafile ${ORDERER_CA} \ - $PEER_CONN_PARMS - -rm log.txt +./network.sh deployCC popd diff --git a/off_chain_data/transferMarble.js b/off_chain_data/transferAsset.js similarity index 75% rename from off_chain_data/transferMarble.js rename to off_chain_data/transferAsset.js index 93af01dc..42c07b6e 100644 --- a/off_chain_data/transferMarble.js +++ b/off_chain_data/transferAsset.js @@ -6,9 +6,9 @@ */ /* - * tranferMarble.js will transfer ownership a specified marble to a new ownder. Example: + * tranferAsset.js will transfer ownership a specified asset to a new ownder. Example: * - * $ node transferMarble.js marble102 jimmy + * $ node transferAsset.js asset102 jimmy * * The utility is meant to demonstrate update block events. */ @@ -25,7 +25,7 @@ const channelid = config.channelid; async function main() { if (process.argv[2] == undefined && process.argv[3] == undefined) { - console.log("Usage: node changeMarbleOwner.js marbleId owner"); + console.log("Usage: node transferAsset.js assetId owner"); process.exit(1); } @@ -34,8 +34,7 @@ async function main() { try { - // Parse the connection profile. This would be the path to the file downloaded - // from the IBM Blockchain Platform operational console. + // Parse the connection profile. const ccpPath = path.resolve(__dirname, '..', 'test-network','organizations','peerOrganizations','org1.example.com', 'connection-org1.json'); const ccp = JSON.parse(fs.readFileSync(ccpPath, 'utf8')); @@ -53,10 +52,10 @@ async function main() { const network = await gateway.getNetwork(channelid); // Get the smart contract from the network channel. - const contract = network.getContract('marbles'); + const contract = network.getContract('basic'); - await contract.submitTransaction('transferMarble', updatekey, newowner); - console.log("Transferred marble " + updatekey + " to " + newowner); + await contract.submitTransaction('TransferAsset', updatekey, newowner); + console.log("Transferred asset " + updatekey + " to " + newowner); await gateway.disconnect(); From a432e7d70ca0ae751f99b095aadc9f99912b634c Mon Sep 17 00:00:00 2001 From: Gaurav Giri Date: Mon, 12 Oct 2020 19:37:15 +0530 Subject: [PATCH 10/38] add setStateBasedEndorsementNOutOf for typescript chaincode (#330) Signed-off-by: Gaurav Giri --- asset-transfer-sbe/README.md | 2 +- .../fabric/samples/sbe/AssetContract.java | 26 ++++--- .../chaincode-typescript/src/assetContract.ts | 68 ++++++++++++++++--- 3 files changed, 75 insertions(+), 21 deletions(-) diff --git a/asset-transfer-sbe/README.md b/asset-transfer-sbe/README.md index 576eda8d..d4b1cad1 100644 --- a/asset-transfer-sbe/README.md +++ b/asset-transfer-sbe/README.md @@ -19,7 +19,7 @@ state-based endorsement, visit the [Endorsement Policies](https://hyperledger-fabric.readthedocs.io/en/release-2.2/endorsement-policies.html) topic in the Fabric documentation. -The implementation provided by State Based interface creates a policy which requires signatures from all the Org principals added, and hence is equivalent to an AND policy. For other advanced State Based policy implementations which are not supported by State Based interface directly like OR or NOutOf policies, please refer to method implementations setAssetStateBasedEndorsementWithNOutOfPolicy(), which can be used as an alternative for setAssetStateBasedEndorsement() inside asset-transfer-sbe smart contracts. +The implementation provided by State Based interface creates a policy which requires signatures from all the Org principals added, and hence is equivalent to an AND policy. For other advanced State Based policy implementations which are not supported by State Based interface directly like OR or NOutOf policies, please refer to method implementations setStateBasedEndorsementNOutOf(), which can be used as an alternative for setStateBasedEndorsement() inside asset-transfer-sbe smart contracts. ## About the Sample diff --git a/asset-transfer-sbe/chaincode-java/src/main/java/org/hyperledger/fabric/samples/sbe/AssetContract.java b/asset-transfer-sbe/chaincode-java/src/main/java/org/hyperledger/fabric/samples/sbe/AssetContract.java index a99d2404..9843ee3b 100644 --- a/asset-transfer-sbe/chaincode-java/src/main/java/org/hyperledger/fabric/samples/sbe/AssetContract.java +++ b/asset-transfer-sbe/chaincode-java/src/main/java/org/hyperledger/fabric/samples/sbe/AssetContract.java @@ -70,11 +70,11 @@ public final class AssetContract implements ContractInterface { String assetJSON = genson.serialize(asset); stub.putStringState(assetId, assetJSON); - // Set the endorsement policy of the assetId Key, such that current owner Org Peer is required to endorse future updates - setAssetStateBasedEndorsement(ctx, assetId, new String[]{ownerOrg}); + // Set the endorsement policy of the assetId Key, such that current owner Org is required to endorse future updates + setStateBasedEndorsement(ctx, assetId, new String[]{ownerOrg}); - // Optionally, set the endorsement policy of the assetId Key, such that any 1(N) out of the Org's specified can endorse future updates - // setAssetStateBasedEndorsementWithNOutOfPolicy(ctx, assetId, 1, new String[]{"Org1MSP", "Org2MSP"}); + // Optionally, set the endorsement policy of the assetId Key, such that any 1 Org (N) out of the specified Orgs can endorse future updates + // setStateBasedEndorsementNOutOf(ctx, assetId, 1, new String[]{"Org1MSP", "Org2MSP"}); return asset; } @@ -164,7 +164,11 @@ public final class AssetContract implements ContractInterface { String updatedAssetJSON = genson.serialize(asset); stub.putStringState(assetId, updatedAssetJSON); - setAssetStateBasedEndorsement(ctx, assetId, new String[]{newOwnerOrg}); + // Re-Set the endorsement policy of the assetId Key, such that a new owner Org Peer is required to endorse future updates + setStateBasedEndorsement(ctx, assetId, new String[]{newOwnerOrg}); + + // Optionally, set the endorsement policy of the assetId Key, such that any 1 Org (N) out of the specified Orgs can endorse future updates + // setStateBasedEndorsementNOutOf(ctx, assetId, 1, new String[]{"Org1MSP", "Org2MSP"}); return asset; } @@ -195,13 +199,13 @@ public final class AssetContract implements ContractInterface { /** * Sets an endorsement policy to the assetId Key. - * Enforces that the owner Org Peers must endorse future update transactions for the specified assetId Key. + * Enforces that the owner Org must endorse future update transactions for the specified assetId Key. * * @param ctx the transaction context * @param assetId the id of the asset * @param ownerOrgs the list of Owner Org MSPID's */ - private static void setAssetStateBasedEndorsement(final Context ctx, final String assetId, final String[] ownerOrgs) { + private static void setStateBasedEndorsement(final Context ctx, final String assetId, final String[] ownerOrgs) { StateBasedEndorsement stateBasedEndorsement = StateBasedEndorsementFactory.getInstance().newStateBasedEndorsement(null); stateBasedEndorsement.addOrgs(StateBasedEndorsement.RoleType.RoleTypeMember, ownerOrgs); ctx.getStub().setStateValidationParameter(assetId, stateBasedEndorsement.policy()); @@ -209,21 +213,21 @@ public final class AssetContract implements ContractInterface { /** * Sets an endorsement policy to the assetId Key. - * Enforces that any N out of the Org's Peers specified must endorse future update transactions for the specified assetId Key. + * Enforces that a given number of Orgs (N) out of the specified Orgs must endorse future update transactions for the specified assetId Key. * * @param ctx the transaction context * @param assetId the id of the asset * @param nOrgs the number of N Orgs to endorse out of the list of Orgs provided * @param ownerOrgs the list of Owner Org MSPID's */ - private static void setAssetStateBasedEndorsementWithNOutOfPolicy(final Context ctx, final String assetId, final int nOrgs, final String[] ownerOrgs) { + private static void setStateBasedEndorsementNOutOf(final Context ctx, final String assetId, final int nOrgs, final String[] ownerOrgs) { ctx.getStub().setStateValidationParameter(assetId, policy(nOrgs, Arrays.asList(ownerOrgs))); } /** - * Create the policy such that it requires any N signature's from all of the principals provided + * Create a policy that requires a given number (N) of Org principals signatures out of the provided list of Orgs * - * @param nOrgs the number of N Org signature's to endorse out of the list of Orgs provided + * @param nOrgs the number of Org principals signatures required to endorse (out of the provided list of Orgs) * @param mspids the list of Owner Org MSPID's */ private static byte[] policy(final int nOrgs, final List mspids) { diff --git a/asset-transfer-sbe/chaincode-typescript/src/assetContract.ts b/asset-transfer-sbe/chaincode-typescript/src/assetContract.ts index 19921a11..493f85cb 100644 --- a/asset-transfer-sbe/chaincode-typescript/src/assetContract.ts +++ b/asset-transfer-sbe/chaincode-typescript/src/assetContract.ts @@ -5,6 +5,7 @@ import { Context, Contract, Info, Transaction } from 'fabric-contract-api'; import { Asset } from './asset'; import { KeyEndorsementPolicy } from 'fabric-shim'; +import * as fabprotos from 'fabric-shim/bundle'; @Info({title: 'AssetContract', description: 'Asset Transfer Smart Contract, using State Based Endorsement(SBE), implemented in TypeScript' }) export class AssetContract extends Contract { @@ -26,8 +27,12 @@ export class AssetContract extends Contract { const buffer = Buffer.from(JSON.stringify(asset)); // Create the asset await ctx.stub.putState(assetId, buffer); - // Set the endorsement policy of the assetId Key, such that current owner Org Peer is required to endorse future updates - await AssetContract.setAssetStateBasedEndorsement(ctx, asset.ID, [ownerOrg]); + + // Set the endorsement policy of the assetId Key, such that current owner Org is required to endorse future updates + await AssetContract.setStateBasedEndorsement(ctx, assetId, [ownerOrg]); + + // Optionally, set the endorsement policy of the assetId Key, such that any 1 Org (N) out of the specified Orgs can endorse future updates + // await AssetContract.setStateBasedEndorsementNOutOf(ctx, assetId, 1, ["Org1MSP", "Org2MSP"]); } // ReadAsset returns asset with given assetId @@ -78,7 +83,10 @@ export class AssetContract extends Contract { // Update the asset await ctx.stub.putState(assetId, Buffer.from(JSON.stringify(asset))); // Re-Set the endorsement policy of the assetId Key, such that a new owner Org Peer is required to endorse future updates - await AssetContract.setAssetStateBasedEndorsement(ctx, asset.ID, [newOwnerOrg]); + await AssetContract.setStateBasedEndorsement(ctx, asset.ID, [newOwnerOrg]); + + // Optionally, set the endorsement policy of the assetId Key, such that any 1 Org (N) out of the specified Orgs can endorse future updates + // await AssetContract.setStateBasedEndorsementNOutOf(ctx, assetId, 1, ["Org1MSP", "Org2MSP"]); } // AssetExists returns true when asset with given ID exists @@ -87,16 +95,58 @@ export class AssetContract extends Contract { return (!!buffer && buffer.length > 0); } - // setAssetStateBasedEndorsement sets an endorsement policy to the assetId Key - // setAssetStateBasedEndorsement enforces that the owner Org Peers must endorse future update transactions for the specified assetId Key - private static async setAssetStateBasedEndorsement(ctx: Context, assetId: string, ownerOrgs: string[]): Promise { + // getClientOrgId gets the client's OrgId (MSPID) + private static getClientOrgId(ctx: Context): string { + return ctx.clientIdentity.getMSPID(); + } + + // setStateBasedEndorsement sets an endorsement policy to the assetId Key + // setStateBasedEndorsement enforces that the owner Org must endorse future update transactions for the specified assetId Key + private static async setStateBasedEndorsement(ctx: Context, assetId: string, ownerOrgs: string[]): Promise { const ep = new KeyEndorsementPolicy(); ep.addOrgs('MEMBER', ...ownerOrgs); await ctx.stub.setStateValidationParameter(assetId, ep.getPolicy()); } - // getClientOrgId gets the client's OrgId (MSPID) - private static getClientOrgId(ctx: Context): string { - return ctx.clientIdentity.getMSPID(); + // setStateBasedEndorsementNOutOf sets an endorsement policy to the assetId Key + // setStateBasedEndorsementNOutOf enforces that a given number of Orgs (N) out of the specified Orgs must endorse future update transactions for the specified assetId Key. + private static async setStateBasedEndorsementNOutOf(ctx: Context, assetId: string, nOrgs:number, ownerOrgs: string[]): Promise { + await ctx.stub.setStateValidationParameter(assetId, AssetContract.policy(nOrgs, ownerOrgs)); + } + + // Create a policy that requires a given number (N) of Org principals signatures out of the provided list of Orgs + private static policy(nOrgs: number, mspIds: string[]): Uint8Array { + const principals = []; + const sigsPolicies = []; + mspIds.forEach((mspId, i) => { + const mspRole = { + role: fabprotos.common.MSPRole.MSPRoleType.MEMBER, + mspIdentifier: mspId + }; + const principal = { + principalClassification: fabprotos.common.MSPPrincipal.Classification.ROLE, + principal: fabprotos.common.MSPRole.encode(mspRole).finish() + }; + principals.push(principal); + const signedBy = { + signedBy: i, + }; + sigsPolicies.push(signedBy); + }); + + // create the policy such that it requires any N signature's from all of the principals provided + const allOf = { + n: nOrgs, + rules: sigsPolicies + }; + const noutof = { + nOutOf: allOf + }; + const spe = { + version: 0, + rule: noutof, + identities: principals + }; + return fabprotos.common.SignaturePolicyEnvelope.encode(spe).finish(); } } From c18a59ceaa7510ba22c9afc89bf118fd027b6296 Mon Sep 17 00:00:00 2001 From: nikhil550 Date: Mon, 12 Oct 2020 14:01:59 -0400 Subject: [PATCH 11/38] Replace private data readme with link to tutorial (#346) Signed-off-by: Nikhil Gupta --- .../chaincode-go/README.md | 247 +----------------- 1 file changed, 1 insertion(+), 246 deletions(-) diff --git a/asset-transfer-private-data/chaincode-go/README.md b/asset-transfer-private-data/chaincode-go/README.md index b6eebfef..de65bbf4 100644 --- a/asset-transfer-private-data/chaincode-go/README.md +++ b/asset-transfer-private-data/chaincode-go/README.md @@ -1,246 +1 @@ -# Private data asset transfer scenario - -The private data asset transfer smart contract uses a simple asset transfer to demonstrate the use of private data collections. All the data that is created by the smart contract is stored in the private data that are collections specified in the `collections_config.json` file: - -- The `assetCollection` is deployed on the peers of Org1 and Org2. This collection is used to store the main asset details, such as the size, color, and owner. The `"memberOnlyRead"` and `"memberOnlyWrite"` parameters are used to specify that only Org1 and Org2 can read and write to this collection. -- The `Org1MSPPrivateCollection` is deployed only on the Org1 peer. Similarly, the `Org2MSPPrivateCollection` is only deployed on the Org2 peer. These organization specific collections are used to store the appraisal value of the asset. This allows the owner of the asset to keep the value of the asset private from other organizations on the channel. The `"endorsementPolicy"` parameter is used to create a collection specific endorsement policy. Each update to `Org1MSPPrivateCollection` or `Org2MSPPrivateCollection` needs to be endorsed by the organization that stores the collection on their peers. - -These three collections are used to transfer the asset between Org1 and Org2. In the tutorial, you will use the private data smart contract to complete the following transfer scenario: - -- A member of Org1 uses the `CreateAsset` function to create a new asset. The `CreateAsset` function reads the certificate information of the client identity that submitted the transaction using the `GetClientIdentity.GetID()` API and creates a new asset with the client identity as the asset owner. The main details of the asset, including the owner, are stored in the `assetCollection` collection. The asset is also created with an appraised value. The appraised value is used by each participant to agree to the transfer of the asset, and is only stored in each organization specific collection. Because the asset is created by a member of Org1, the initial appraisal value agreed by the asset owner is stored in the `Org1MSPPrivateCollection`. -- A member of Org2 creates an agreement to trade using the `AgreeToTransfer` function. The potential buyer uses this function to agree to an appraisal value. The value is stored in `Org2MSPPrivateCollection`, and can only read by a member of Org2. The `AgreeToTransfer` function also uses the `GetClientIdentity().GetID()` API to read the client identity that is agreeing to the Transfer. The **TransferAgreement** is stored in the `assetCollection` as a key with the name "transferAgreement{assetID}" -- After the member of Org2 has agreed to the transfer, the asset owner can transfer the asset to the buyer using the `TransferAsset` function. The smart contract completes a couple of checks before the asset is transferred: - - The transfer request is submitted by the owner of the asset. - - The smart contract uses the `GetPrivateDataHash()` function to check that the hash of the asset appraisal value in `Org1MSPPrivateCollection` matches the hash of the appraisal value in the `Org2MSPPrivateCollection`. If the hashes are the same, it confirms that the owner and the interested buyer have agreed to the same asset value. - If both conditions are met, the transfer function will get the client ID of the buyer from the transfer agreement and make the buyer the new owner of the asset. The transfer function will also delete the asset appraisal value from the collection of the former owner, as well as remove the transfer agreement from the `assetCollection`. - -The private data asset transfer enabled by this smart contract is meant to demonstrate the use private data collections. For an example of a more realistic transfer scenario, see the [secure asset transfer smart contract](../../asset-transfer-secured-agreement/chaincode-go). - -## Deploy the smart contract to the test network - -You can run the private data transfer scenario using the Fabric test network. Open a command terminal and navigate to test network directory in your local clone of the `fabric-samples`. We will operate from the `test-network` directory for the remainder of the tutorial. -``` -cd fabric-samples/test-network -``` - -Run the following command to deploy the test network: - -``` -./network.sh up createChannel -ca -s couchdb -``` - -The test network is deployed with two peer organizations. The `createChannel` flag deploys the network with a single channel named `mychannel` with Org1 and Org2 as channel members. The `-ca` flag is used to deploy the network using certificate authorities. This allows you to use each organization's CA to register and enroll new users for this tutorial. - -## Deploy the smart contract to the channel - -You can use the test network script to deploy the private data smart contract to the channel that was just created. Deploy the smart contract to `mychannel` using the following command: -``` -./network.sh deployCC -ccn private -ccep "OR('Org1MSP.peer','Org2MSP.peer')" -cccg ../asset-transfer-private-data/chaincode-go/collections_config.json -``` - -The above command deploys the go chaincode with short name `private`, and specifies the private data collection configuration from file `collections_config.json` using `-cccg` flag. -Note that we are using the `-ccep` flag to deploy the private data smart contract with a chaincode endorsement policy of `"OR('Org1MSP.peer','Org2MSP.peer')"`. This allows Org1 and Org2 to create an asset without receiving an endorsement from the other organization. - -Now you are ready to call the deployed smart contract. -Note that this sample workflow steps below, can also be executed via the application at `asset-transfer-private-data/application-javascript` folder, in fewer steps. To execute the workflow via CLI, read on. - -## Register identities - -The private data transfer smart contract supports ownership by individual identities that belong to the network. In our scenario, the owner of the asset will be a member of Org1, while the buyer will belong to Org2. To highlight the connection between the `GetClientIdentity().GetID()` API and the information within a user's certificate, we will register two new identities using the Org1 and Org2 Certificate Authorities (CA's), and then use the CA's to generate each identity's certificate and private key. - -First, we need to set the following environment variables to use the the Fabric CA client: -``` -export PATH=${PWD}/../bin:${PWD}:$PATH -export FABRIC_CFG_PATH=$PWD/../config/ -``` - -We will use the Org1 CA to create the identity asset owner. Set the Fabric CA client home to the MSP of the Org1 CA admin (this identity was generated by the test network script): -``` -export FABRIC_CA_CLIENT_HOME=${PWD}/organizations/peerOrganizations/org1.example.com/ -``` - -You can register a new owner client identity using the `fabric-ca-client` tool: -``` -fabric-ca-client register --caname ca-org1 --id.name owner --id.secret ownerpw --id.type client --tls.certfiles ${PWD}/organizations/fabric-ca/org1/tls-cert.pem -``` - -You can now generate the identity certificates and MSP folder by providing the enroll name and secret to the enroll command: -``` -fabric-ca-client enroll -u https://owner:ownerpw@localhost:7054 --caname ca-org1 -M ${PWD}/organizations/peerOrganizations/org1.example.com/users/owner@org1.example.com/msp --tls.certfiles ${PWD}/organizations/fabric-ca/org1/tls-cert.pem -``` - -Run the command below to copy the Node OU configuration file into the owner identity MSP folder. -``` -cp ${PWD}/organizations/peerOrganizations/org1.example.com/msp/config.yaml ${PWD}/organizations/peerOrganizations/org1.example.com/users/owner@org1.example.com/msp/config.yaml -``` - -We can now use the Org2 CA to create the buyer identity. Set the Fabric CA client home the Org2 CA admin: -``` -export FABRIC_CA_CLIENT_HOME=${PWD}/organizations/peerOrganizations/org2.example.com/ -``` - -You can register a new owner client identity using the `fabric-ca-client` tool: -``` -fabric-ca-client register --caname ca-org2 --id.name buyer --id.secret buyerpw --id.type client --tls.certfiles ${PWD}/organizations/fabric-ca/org2/tls-cert.pem -``` - -We can now enroll to generate the identity MSP folder: -``` -fabric-ca-client enroll -u https://buyer:buyerpw@localhost:8054 --caname ca-org2 -M ${PWD}/organizations/peerOrganizations/org2.example.com/users/buyer@org2.example.com/msp --tls.certfiles ${PWD}/organizations/fabric-ca/org2/tls-cert.pem -``` - -Run the command below to copy the Node OU configuration file into the buyer identity MSP folder. -``` -cp ${PWD}/organizations/peerOrganizations/org2.example.com/msp/config.yaml ${PWD}/organizations/peerOrganizations/org2.example.com/users/buyer@org2.example.com/msp/config.yaml -``` - -## Create an asset - -Now that we have created the identity of the asset owner, we can invoke the private data smart contract to create a new asset. Use the following environment variables to operate the `peer` CLI as the owner identity from Org1. - -``` -export CORE_PEER_TLS_ENABLED=true -export CORE_PEER_LOCALMSPID="Org1MSP" -export CORE_PEER_MSPCONFIGPATH=${PWD}/organizations/peerOrganizations/org1.example.com/users/owner@org1.example.com/msp -export CORE_PEER_TLS_ROOTCERT_FILE=${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt -export CORE_PEER_ADDRESS=localhost:7051 -``` - -Run the following command to define the asset properties: -``` -export ASSET_PROPERTIES=$(echo -n "{\"objectType\":\"asset\",\"assetID\":\"asset1\",\"color\":\"green\",\"size\":20,\"appraisedValue\":100}" | base64 | tr -d \\n) -``` - -We can then invoke the smart contract to create the new asset: -``` -peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n private -c '{"function":"CreateAsset","Args":[]}' --transient "{\"asset_properties\":\"$ASSET_PROPERTIES\"}" -``` - -The command above uses the transient data flag, `--transient`, to provide the asset details to the smart contract. Transient data is not part of the transaction read/write set, and as result is not stored on the channel ledger. - -Note that command above only targets the Org1 peer. The `CreateAsset` transactions writes to two collections, `assetCollection` and `Org1MSPPrivateCollection`. The `Org1MSPPrivateCollection` requires and endorsement from the Org1 peer in order to write to the collection, while the `assetCollection` inherits the endorsement policy of the chaincode, `"OR('Org1MSP.peer','Org2MSP.peer')"`. An endorsement from the Org1 peer can meet both endorsement policies and is able to create an asset without an endorsement from Org2. - -We can read the main details of the asset that was created by using the `ReadAsset` function to query the `assetCollection` collection: -``` -peer chaincode query -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n private -c '{"function":"ReadAsset","Args":["asset1"]}' -``` - -When successful, the command will return the following result: -``` -{"objectType":"asset","assetID":"asset1","color":"green","size":20,"owner":"eDUwOTo6Q049b3duZXIsT1U9Y2xpZW50LE89SHlwZXJsZWRnZXIsU1Q9Tm9ydGggQ2Fyb2xpbmEsQz1VUzo6Q049Y2Eub3JnMS5leGFtcGxlLmNvbSxPPW9yZzEuZXhhbXBsZS5jb20sTD1EdXJoYW0sU1Q9Tm9ydGggQ2Fyb2xpbmEsQz1VUw=="} -``` - -The `"owner"` of the asset is the identity that created the asset by invoking the smart contract. The `GetClientIdentity().GetID()` API reads the common name and issuer of the identity certificate. You can see that information by decoding the owner string out of base64 format: -``` -echo eDUwOTo6Q049b3duZXIsT1U9Y2xpZW50LE89SHlwZXJsZWRnZXIsU1Q9Tm9ydGggQ2Fyb2xpbmEsQz1VUzo6Q049Y2Eub3JnMS5leGFtcGxlLmNvbSxPPW9yZzEuZXhhbXBsZS5jb20sTD1EdXJoYW0sU1Q9Tm9ydGggQ2Fyb2xpbmEsQz1VUw | base64 --decode -``` - -The result will show the common name and issuer of the owner certificate: -``` -x509::CN=owner,OU=client,O=Hyperledger,ST=North Carolina,C=US::CN=ca.org1.example.com,O=org1.example.com,L=Durham,ST=North Carolina,C=Umacbook-air:test-network -``` - -A member of Org1 can also read the private asset appraisal value that is stored in the `Org1MSPPrivateCollection` on the Org1 peer: -``` -peer chaincode query -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n private -c '{"function":"ReadAssetPrivateDetails","Args":["Org1MSPPrivateCollection","asset1"]}' -``` -The query will return the value of the asset: -``` -{"assetID":"asset1","appraisedValue":100} -``` - -### Buyer from Org2 agrees to buy the asset - -The buyer identity from Org2 is interested in buying the asset. In a new terminal, set the following environment variables to operate as the buyer: - -``` -export CORE_PEER_LOCALMSPID="Org2MSP" -export CORE_PEER_MSPCONFIGPATH=${PWD}/organizations/peerOrganizations/org2.example.com/users/buyer@org2.example.com/msp -export CORE_PEER_TLS_ROOTCERT_FILE=${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt -export CORE_PEER_ADDRESS=localhost:9051 -``` - -Now that we are operating as a member of Org2, we can demonstrate that the asset appraisal is not stored in Org2MSPPrivateCollection, on the Org2 peer: -``` -peer chaincode query -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n private -c '{"function":"ReadAssetPrivateDetails","Args":["Org2MSPPrivateCollection","asset1"]}' -``` -The empty response shows that, the asset1 private details, does not exist in buyer private collection. - -Nor can a member of Org2, able to read the Org1 private data collection: -``` -peer chaincode query -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n private -c '{"function":"ReadAssetPrivateDetails","Args":["Org1MSPPrivateCollection","asset1"]}' -``` -By setting `"memberOnlyRead": true` in the collection configuration file, we specify that only members of Org1 can read data from the collection. A Org2 member who tries to read the collection would only get the following response. -``` -Error: endorsement failure during query. response: status:500 message:"failed to read from asset details GET_STATE failed: transaction ID: 10d39a7d0b340455a19ca4198146702d68d884d41a0e60936f1599c1ddb9c99d: tx creator does not have read access permission on privatedata in chaincodeName:private collectionName: Org1MSPPrivateCollection" -``` - -To purchase the asset, the buyer needs to agree to the same value as the asset owner. The agreed value will be stored in the `Org2MSPDetailsCollection` collection on the Org2 peer. Run the following command to agree to the appraised value of 100: -``` -export ASSET_VALUE=$(echo -n "{\"assetID\":\"asset1\",\"appraisedValue\":100}" | base64 | tr -d \\n) -peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n private -c '{"function":"AgreeToTransfer","Args":[]}' --transient "{\"asset_value\":\"$ASSET_VALUE\"}" -``` - -The buyer can now query the value they agreed to in the Org2 private data collection: -``` -peer chaincode query -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n private -c '{"function":"ReadAssetPrivateDetails","Args":["Org2MSPPrivateCollection","asset1"]}' -``` -The invoke will return the following value: -``` -{"assetID":"asset1","appraisedValue":100} -``` - -## Org1 member transfers the asset to Org2 - -Now that buyer has agreed to buy the asset for appraised value, the owner from Org1 can transfer the asset to Org2. In the first terminal (with the following environment variables to operate as Org1): -``` -export CORE_PEER_LOCALMSPID="Org1MSP" -export CORE_PEER_MSPCONFIGPATH=${PWD}/organizations/peerOrganizations/org1.example.com/users/owner@org1.example.com/msp -export CORE_PEER_TLS_ROOTCERT_FILE=${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt -export CORE_PEER_ADDRESS=localhost:7051 -``` - -Now that buyer has agreed to buy the asset for appraised value, the owner from Org1 can read the data added by `AgreeToTransfer` to see buyer identity. -``` -peer chaincode query -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n private -c '{"function":"ReadTransferAgreement","Args":["asset1"]}' -``` - -The owner from Org1 can now transfer the asset to Org2. To transfer the asset, the owner needs to pass the MSP ID of new asset owner Org. The transfer function will read the client ID of the interested buyer user from the transfer agreement. -``` -export ASSET_OWNER=$(echo -n "{\"assetID\":\"asset1\",\"buyerMSP\":\"Org2MSP\"}" | base64 | tr -d \\n) -``` - -The owner of the asset needs to initiate the transfer. - -``` -peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n private -c '{"function":"TransferAsset","Args":[]}' --transient "{\"asset_owner\":\"$ASSET_OWNER\"}" --peerAddresses localhost:7051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt -``` -You can ReadAsset `asset1` to see the results of the transfer. -``` -peer chaincode query -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n private -c '{"function":"ReadAsset","Args":["asset1"]}' -``` - -The results will show that the buyer identity now owns the asset: - -``` -{"objectType":"asset","assetID":"asset1","color":"green","size":20,"owner":"eDUwOTo6Q049YnV5ZXIsT1U9Y2xpZW50LE89SHlwZXJsZWRnZXIsU1Q9Tm9ydGggQ2Fyb2xpbmEsQz1VUzo6Q049Y2Eub3JnMi5leGFtcGxlLmNvbSxPPW9yZzIuZXhhbXBsZS5jb20sTD1IdXJzbGV5LFNUPUhhbXBzaGlyZSxDPVVL"} -``` - -You can base64 decode the `"owner"` to see that it is the buyer identity: -``` -x509::CN=buyer,OU=client,O=Hyperledger,ST=North Carolina,C=US::CN=ca.org2.example.com,O=org2.example.com,L=Hursley,ST=Hampshire,C=UKmacbook-air -``` - -You can also confirm that transfer removed the private details from the Org1 collection: -``` -peer chaincode query -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n private -c '{"function":"ReadAssetPrivateDetails","Args":["Org1MSPPrivateCollection","asset1"]}' -``` -Your query will return empty result, since the asset private data is removed from the Org1 private data collection. - -## Clean up - -When you are finished, you can bring down the test network. The command will remove all the nodes of the test network, and delete any ledger data that you created: - -``` -./network.sh down -``` +[Using Private Data tutorial](https://hyperledger-fabric.readthedocs.io/en/latest/private_data_tutorial.html) \ No newline at end of file From e7189fc62a50871c276f7a2f31cf18c99330ed1b Mon Sep 17 00:00:00 2001 From: Sijo Cherian Date: Sat, 10 Oct 2020 23:15:47 -0400 Subject: [PATCH 12/38] bugfix: asset type field name in couchdb index def Signed-off-by: Sijo Cherian --- .../assetCollection/indexes/indexOwner.json | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/asset-transfer-private-data/chaincode-go/META-INF/statedb/couchdb/collections/assetCollection/indexes/indexOwner.json b/asset-transfer-private-data/chaincode-go/META-INF/statedb/couchdb/collections/assetCollection/indexes/indexOwner.json index cef82360..e2d1d087 100644 --- a/asset-transfer-private-data/chaincode-go/META-INF/statedb/couchdb/collections/assetCollection/indexes/indexOwner.json +++ b/asset-transfer-private-data/chaincode-go/META-INF/statedb/couchdb/collections/assetCollection/indexes/indexOwner.json @@ -1 +1,12 @@ -{"index":{"fields":["type","owner"]},"ddoc":"indexOwnerDoc", "name":"indexOwner","type":"json"} +{ + "index": { + "fields": [ + "objectType", + "owner" + ] + }, + "ddoc": "indexOwnerDoc", + "name": "indexOwner", + "type": "json" +} + From fa0529c393015dd3e533f07a35ad910cdcb1eb82 Mon Sep 17 00:00:00 2001 From: denyeart Date: Wed, 14 Oct 2020 02:26:38 -0400 Subject: [PATCH 13/38] Add account-based token sample (go chaincode) (#318) Add an account-based token sample go chaincode, as well as an associated readme tutorial. Signed-off-by: David Enyeart --- token-account-based/README.md | 202 ++++++++++++++++++ .../chaincode-go/chaincode/token_contract.go | 174 +++++++++++++++ token-account-based/chaincode-go/go.mod | 5 + token-account-based/chaincode-go/go.sum | 145 +++++++++++++ .../chaincode-go/token_account_based.go | 23 ++ 5 files changed, 549 insertions(+) create mode 100644 token-account-based/README.md create mode 100644 token-account-based/chaincode-go/chaincode/token_contract.go create mode 100644 token-account-based/chaincode-go/go.mod create mode 100644 token-account-based/chaincode-go/go.sum create mode 100644 token-account-based/chaincode-go/token_account_based.go diff --git a/token-account-based/README.md b/token-account-based/README.md new file mode 100644 index 00000000..307b82f6 --- /dev/null +++ b/token-account-based/README.md @@ -0,0 +1,202 @@ +# Account-based token scenario + +The account-based token smart contract demonstrates how to create and transfer fungible tokens using an account-based model. In an account-based model, there is an account for each participant that holds a balance of tokens. +A mint transaction creates tokens in an account, while a transfer transaction debits the caller's account and credits another account. + +In this sample it is assumed that only one organization (played by Org1) is in a central banker role and can mint new tokens into their account, while any organization can transfer tokens from their account to a recipient's account. +Accounts could be defined at the organization level or client identity level. In this sample accounts are defined at the client identity level, where every authorized client with an enrollment certificate from their organization implicitly has an account ID that matches their client ID. +The client ID is simply a base64-encoded concatenation of the issuer and subject from the client identity's enrollment certificate. The client ID can therefore be considered the account ID that is used as the payment address of a recipient. + +In this tutorial, you will mint and transfer tokens as follows: + +- A member of Org1 uses the `Mint` function to create new tokens into their account. The `Mint` function reads the certificate information of the client identity that submitted the transaction using the `GetClientIdentity.GetID()` API and credits the account associated with the client ID with the requested number of tokens. +- The same minter client will then use the `Transfer` function to transfer the requested number of tokens to the recipient's account. It is assumed that the recipient has provided their account ID to the transfer caller out of band. The recipient can then transfer tokens to other registered users in the same fashion. + +## Bring up the test network + +You can run the account-based token transfer scenario using the Fabric test network. Open a command terminal and navigate to the test network directory in your local clone of the `fabric-samples`. We will operate from the `test-network` directory for the remainder of the tutorial. +``` +cd fabric-samples/test-network +``` + +Run the following command to start the test network: +``` +./network.sh up createChannel -ca +``` + +The test network is deployed with two peer organizations. The `createChannel` flag deploys the network with a single channel named `mychannel` with Org1 and Org2 as channel members. +The -ca flag is used to deploy the network using certificate authorities. This allows you to use each organization's CA to register and enroll new users for this tutorial. + +## Deploy the smart contract to the channel + +You can use the test network script to deploy the account-based token contract to the channel that was just created. Deploy the smart contract to `mychannel` using the following command: +``` +./network.sh deployCC -ccn token_account -ccp ../token-account-based/chaincode-go/ +``` + +The above command deploys the go chaincode with short name `token_account`. The smart contract will use the default endorsement policy of majority of channel members. +Since the channel has two members, this implies that we'll need to get peer endorsements from 2 out of the 2 channel members. + +Now you are ready to call the deployed smart contract via peer CLI calls. But let's first create the client identities for our scenario. + +## Register identities + +The smart contract supports accounts owned by individual client identities from organizations that are members of the channel. In our scenario, the minter of the tokens will be a member of Org1, while the recipient will belong to Org2. To highlight the connection between the `GetClientIdentity().GetID()` API and the information within a user's certificate, we will register two new identities using the Org1 and Org2 Certificate Authorities (CA's), and then use the CA's to generate each identity's certificate and private key. + +First, we need to set the following environment variables to use the the Fabric CA client (and subsequent commands). +``` +export PATH=${PWD}/../bin:${PWD}:$PATH +export FABRIC_CFG_PATH=$PWD/../config/ +``` + +The terminal we have been using will represent Org1. We will use the Org1 CA to create the minter identity. Set the Fabric CA client home to the MSP of the Org1 CA admin (this identity was generated by the test network script): +``` +export FABRIC_CA_CLIENT_HOME=${PWD}/organizations/peerOrganizations/org1.example.com/ +``` + +You can register a new minter client identity using the `fabric-ca-client` tool: +``` +fabric-ca-client register --caname ca-org1 --id.name minter --id.secret minterpw --id.type client --tls.certfiles ${PWD}/organizations/fabric-ca/org1/tls-cert.pem +``` + +You can now generate the identity certificates and MSP folder by providing the enroll name and secret to the enroll command: +``` +fabric-ca-client enroll -u https://minter:minterpw@localhost:7054 --caname ca-org1 -M ${PWD}/organizations/peerOrganizations/org1.example.com/users/minter@org1.example.com/msp --tls.certfiles ${PWD}/organizations/fabric-ca/org1/tls-cert.pem +``` + +Run the command below to copy the Node OU configuration file into the minter identity MSP folder. +``` +cp ${PWD}/organizations/peerOrganizations/org1.example.com/msp/config.yaml ${PWD}/organizations/peerOrganizations/org1.example.com/users/minter@org1.example.com/msp/config.yaml +``` + +Open a new terminal to represent Org2 and navigate to /fabric-samples/test-network. We'll use the Org2 CA to create the Org2 recipient identity. Set the Fabric CA client home to the MSP of the Org2 CA admin: +``` +cd fabric-samples/test-network +export FABRIC_CA_CLIENT_HOME=${PWD}/organizations/peerOrganizations/org2.example.com/ +``` + +You can register a recipient client identity using the `fabric-ca-client` tool: +``` +fabric-ca-client register --caname ca-org2 --id.name recipient --id.secret recipientpw --id.type client --tls.certfiles ${PWD}/organizations/fabric-ca/org2/tls-cert.pem +``` + +We can now enroll to generate the identity certificates and MSP folder: +``` +fabric-ca-client enroll -u https://recipient:recipientpw@localhost:8054 --caname ca-org2 -M ${PWD}/organizations/peerOrganizations/org2.example.com/users/recipient@org2.example.com/msp --tls.certfiles ${PWD}/organizations/fabric-ca/org2/tls-cert.pem +``` + +Run the command below to copy the Node OU configuration file into the recipient identity MSP folder. +``` +cp ${PWD}/organizations/peerOrganizations/org2.example.com/msp/config.yaml ${PWD}/organizations/peerOrganizations/org2.example.com/users/recipient@org2.example.com/msp/config.yaml +``` + +## Mint some tokens + +Now that we have created the identity of the minter, we can invoke the smart contract to mint some tokens. +Shift back to the Org1 terminal, we'll set the following environment variables to operate the `peer` CLI as the minter identity from Org1. +``` +export CORE_PEER_TLS_ENABLED=true +export CORE_PEER_LOCALMSPID="Org1MSP" +export CORE_PEER_MSPCONFIGPATH=${PWD}/organizations/peerOrganizations/org1.example.com/users/minter@org1.example.com/msp +export CORE_PEER_TLS_ROOTCERT_FILE=${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt +export CORE_PEER_ADDRESS=localhost:7051 +export TARGET_TLS_OPTIONS="-o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem --peerAddresses localhost:7051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt --peerAddresses localhost:9051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt" +``` + +The last environment variable above will be utilized within the CLI invoke commands to set the target peers for endorsement, and the target ordering service endpoint and TLS options. + +We can then invoke the smart contract to mint 5000 tokens: +``` +peer chaincode invoke $TARGET_TLS_OPTIONS -C mychannel -n token_account -c '{"function":"Mint","Args":["5000"]}' +``` + +The mint function validated that the client is a member of the minter organization, and then credited the minter client's account with 5000 tokens. We can check the minter client's account balance by calling the `ClientAccountBalance` function. +``` +peer chaincode query -C mychannel -n token_account -c '{"function":"ClientAccountBalance","Args":[]}' +``` + +The function queries the balance of the account associated with the minter client ID and returns: +``` +5000 +``` + +## Transfer tokens + +The minter intends to transfer 100 tokens to the Org2 recipient, but first the Org2 recipient needs to provide their own account ID as the payment address. +A client can derive their account ID from their own public certificate, but to be sure the account ID is accurate, the contract has a `ClientAccountID` utility function that simply looks at the callers certificate and returns the calling client's ID, which will be used as the account ID. +Let's prepare the Org2 terminal by setting the environment variables for the Org2 recipient user. +``` +export PATH=${PWD}/../bin:${PWD}:$PATH +export FABRIC_CFG_PATH=$PWD/../config/ +export CORE_PEER_TLS_ENABLED=true +export CORE_PEER_LOCALMSPID="Org2MSP" +export CORE_PEER_MSPCONFIGPATH=${PWD}/organizations/peerOrganizations/org2.example.com/users/recipient@org2.example.com/msp +export CORE_PEER_TLS_ROOTCERT_FILE=${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt +export CORE_PEER_ADDRESS=localhost:9051 +``` + +Using the Org2 terminal, the Org2 recipient user can retrieve their own account ID: +``` +peer chaincode query -C mychannel -n token_account -c '{"function":"ClientAccountID","Args":[]}' +``` + +The function returns of recipient's account ID: +``` +eDUwOTo6Q049cmVjaXBpZW50LE9VPWNsaWVudCxPPUh5cGVybGVkZ2VyLFNUPU5vcnRoIENhcm9saW5hLEM9VVM6OkNOPWNhLm9yZzIuZXhhbXBsZS5jb20sTz1vcmcyLmV4YW1wbGUuY29tLEw9SHVyc2xleSxTVD1IYW1wc2hpcmUsQz1VSw== +``` + +Let's base64 decode the account ID to make sure it represents the Org2 recipient user: +``` +echo eDUwOTo6Q049cmVjaXBpZW50LE9VPWNsaWVudCxPPUh5cGVybGVkZ2VyLFNUPU5vcnRoIENhcm9saW5hLEM9VVM6OkNOPWNhLm9yZzIuZXhhbXBsZS5jb20sTz1vcmcyLmV4YW1wbGUuY29tLEw9SHVyc2xleSxTVD1IYW1wc2hpcmUsQz1VSw== | base64 --decode +``` + +The result shows that the subject and issuer is indeed the recipient user from Org2: +``` +x509::CN=recipient,OU=client,O=Hyperledger,ST=North Carolina,C=US::CN=ca.org2.example.com,O=org2.example.com,L=Hursley,ST=Hampshire,C=UK +``` + +After the Org2 recipient provides their account ID to the minter, the minter can initiate a transfer from their account to the recipient's account. +Back in the Org1 terminal, request the transfer of 100 tokens to the recipient account: +``` +peer chaincode invoke $TARGET_TLS_OPTIONS -C mychannel -n token_account -c '{"function":"Transfer","Args":[ "eDUwOTo6Q049cmVjaXBpZW50LE9VPWNsaWVudCxPPUh5cGVybGVkZ2VyLFNUPU5vcnRoIENhcm9saW5hLEM9VVM6OkNOPWNhLm9yZzIuZXhhbXBsZS5jb20sTz1vcmcyLmV4YW1wbGUuY29tLEw9SHVyc2xleSxTVD1IYW1wc2hpcmUsQz1VSw==","100"]}' +``` + +The `Transfer` function validates that the account associated with the calling client ID has sufficient funds for the transfer. +It will then debit the caller's account and credit the recipient's account. Note that the sample contract will automatically create an account with zero balance for the recipient, if one does not yet exist. + +While still in the Org1 terminal, let's request the minter's account balance again: +``` +peer chaincode query -C mychannel -n token_account -c '{"function":"ClientAccountBalance","Args":[]}' +``` + +The function queries the balance of the account associated with the minter client ID and returns: +``` +4900 +``` + +And then using the Org2 terminal, let's request the recipient's balance: +``` +peer chaincode query -C mychannel -n token_account -c '{"function":"ClientAccountBalance","Args":[]}' +``` + +The function queries the balance of the account associated with the recipient client ID and returns: +``` +100 +``` + +Congratulations, you've transferred 100 tokens! The Org2 recipient can now transfer tokens to other registered users in the same manner. + +## Clean up + +When you are finished, you can bring down the test network. The command will remove all the nodes of the test network, and delete any ledger data that you created: +``` +./network.sh down +``` + +## Contract extension ideas + +You can extend the basic account-based token sample to meet other requirements. For example: + +* Rather than using the default 'majority' endorsement policy, you could set the endorsement policy to a subset of organizations that represent trust anchors for the contract execution. +* You could also require that accounts get setup before use, and apply state-based endorsement for each account key that has been created. For example on an OrgA account, set state-based endorsement policy to be OrgA and the central banker (or some other trust anchor). And on an OrgB account, set state-based endorsement policy to be OrgB and the central banker (or some other trust anchor). Then to transfer tokens from an OrgA account to an OrgB account, you would require endorsements from OrgA, OrgB, and the central banker (or some other trust anchor). +* You could utilize anonymous addresses for accounts based on private-public key pairs, instead of accounts keyed by the client ID. In order to spend the tokens, the client would have to sign the transfer input as proof that they own the address private key, which the contract would then validate, similar to the Ethereum model in the permissionless blockchain space. However, in a permissioned blockchain such as Fabric, only registered clients are authorized to participate. Furthermore, if you don't want to leak the registered client identity associated with each account, the clients could be registered using an Identity Mixer MSP, so that the client itself is also anonymous in each of the token transactions. diff --git a/token-account-based/chaincode-go/chaincode/token_contract.go b/token-account-based/chaincode-go/chaincode/token_contract.go new file mode 100644 index 00000000..f9f34e8c --- /dev/null +++ b/token-account-based/chaincode-go/chaincode/token_contract.go @@ -0,0 +1,174 @@ +package chaincode + +import ( + "fmt" + "log" + "strconv" + + "github.com/hyperledger/fabric-contract-api-go/contractapi" +) + +// SmartContract provides functions for transferring tokens between accounts +type SmartContract struct { + contractapi.Contract +} + +// Mint creates new tokens and adds them to minter's account balance +func (s *SmartContract) Mint(ctx contractapi.TransactionContextInterface, amount int) error { + + // Check minter authorization - this sample assumes Org1 is the central banker with privilege to mint new tokens + clientMSPID, err := ctx.GetClientIdentity().GetMSPID() + if err != nil { + return fmt.Errorf("failed to get MSPID: %v", err) + } + if clientMSPID != "Org1MSP" { + return fmt.Errorf("client is not authorized to mint new tokens") + } + + // Get ID of submitting client identity + minter, err := ctx.GetClientIdentity().GetID() + if err != nil { + return fmt.Errorf("failed to get client id: %v", err) + } + + if amount <= 0 { + return fmt.Errorf("mint amount must be a positive integer") + } + + currentBalanceBytes, err := ctx.GetStub().GetState(minter) + if err != nil { + return fmt.Errorf("failed to read minter account %s from world state: %v", minter, err) + } + + var currentBalance int + + // If minter current balance doesn't yet exist, we'll create it with a current balance of 0 + if currentBalanceBytes == nil { + currentBalance = 0 + } else { + currentBalance, _ = strconv.Atoi(string(currentBalanceBytes)) // Error handling not needed since Itoa() was used when setting the account balance, guaranteeing it was an integer. + } + + updatedBalance := currentBalance + amount + + err = ctx.GetStub().PutState(minter, []byte(strconv.Itoa(updatedBalance))) + if err != nil { + return err + } + + log.Printf("minter account %s balance updated from %d to %d", minter, currentBalance, updatedBalance) + + return nil +} + +// Transfer transfers tokens from client account to recipient account. +// recipient account must be a valid clientID as returned by the ClientID() function. +func (s *SmartContract) Transfer(ctx contractapi.TransactionContextInterface, recipient string, amount int) error { + + if amount < 0 { // transfer of 0 is allowed in ERC20, so just validate against negative amounts + return fmt.Errorf("transfer amount cannot be negative") + } + + // Get ID of submitting client identity + clientID, err := ctx.GetClientIdentity().GetID() + if err != nil { + return fmt.Errorf("failed to get client id: %v", err) + } + + clientCurrentBalanceBytes, err := ctx.GetStub().GetState(clientID) + if err != nil { + return fmt.Errorf("failed to read client account %s from world state: %v", clientID, err) + } + + if clientCurrentBalanceBytes == nil { + return fmt.Errorf("client account %s has no balance", clientID) + } + + clientCurrentBalance, _ := strconv.Atoi(string(clientCurrentBalanceBytes)) // Error handling not needed since Itoa() was used when setting the account balance, guaranteeing it was an integer. + + if clientCurrentBalance < amount { + return fmt.Errorf("client account %s has insufficient funds", clientID) + } + + recipientCurrentBalanceBytes, err := ctx.GetStub().GetState(recipient) + if err != nil { + fmt.Errorf("failed to read recipient account %s from world state: %v", recipient, err) + } + + var recipientCurrentBalance int + // If recipient current balance doesn't yet exist, we'll create it with a current balance of 0 + if recipientCurrentBalanceBytes == nil { + recipientCurrentBalance = 0 + } else { + recipientCurrentBalance, _ = strconv.Atoi(string(recipientCurrentBalanceBytes)) // Error handling not needed since Itoa() was used when setting the account balance, guaranteeing it was an integer. + } + + clientUpdatedBalance := clientCurrentBalance - amount + recipientUpdatedBalance := recipientCurrentBalance + amount + + err = ctx.GetStub().PutState(clientID, []byte(strconv.Itoa(clientUpdatedBalance))) + if err != nil { + return err + } + + err = ctx.GetStub().PutState(recipient, []byte(strconv.Itoa(recipientUpdatedBalance))) + if err != nil { + return err + } + + log.Printf("client %s balance updated from %d to %d", clientID, clientCurrentBalance, clientUpdatedBalance) + log.Printf("recipient %s balance updated from %d to %d", recipient, recipientCurrentBalance, recipientUpdatedBalance) + + return nil +} + +// BalanceOf returns the balance of the given account +func (s *SmartContract) BalanceOf(ctx contractapi.TransactionContextInterface, account string) (int, error) { + balanceBytes, err := ctx.GetStub().GetState(account) + if err != nil { + return 0, fmt.Errorf("failed to read from world state: %v", err) + } + if balanceBytes == nil { + return 0, fmt.Errorf("the account %s does not exist", account) + } + + balance, _ := strconv.Atoi(string(balanceBytes)) // Error handling not needed since Itoa() was used when setting the account balance, guaranteeing it was an integer. + + return balance, nil +} + +// ClientAccountBalance returns the balance of the requesting client's account +func (s *SmartContract) ClientAccountBalance(ctx contractapi.TransactionContextInterface) (int, error) { + + // Get ID of submitting client identity + clientID, err := ctx.GetClientIdentity().GetID() + if err != nil { + return 0, fmt.Errorf("failed to get client id: %v", err) + } + + balanceBytes, err := ctx.GetStub().GetState(clientID) + if err != nil { + return 0, fmt.Errorf("failed to read from world state: %v", err) + } + if balanceBytes == nil { + return 0, fmt.Errorf("the account %s does not exist", clientID) + } + + balance, _ := strconv.Atoi(string(balanceBytes)) // Error handling not needed since Itoa() was used when setting the account balance, guaranteeing it was an integer. + + return balance, nil +} + +// ClientAccountID returns the id of the requesting client's account. +// In this implementation, the client account ID is the clientId itself. +// Users can use this function to get their own account id, which they can then give to others as the payment address +func (s *SmartContract) ClientAccountID(ctx contractapi.TransactionContextInterface) (string, error) { + + // Get ID of submitting client identity + clientAccountID, err := ctx.GetClientIdentity().GetID() + if err != nil { + return "", fmt.Errorf("failed to get client id: %v", err) + } + + return clientAccountID, nil +} diff --git a/token-account-based/chaincode-go/go.mod b/token-account-based/chaincode-go/go.mod new file mode 100644 index 00000000..cac66472 --- /dev/null +++ b/token-account-based/chaincode-go/go.mod @@ -0,0 +1,5 @@ +module github.com/hyperledger/fabric-samples/token-account-based/chaincode-go + +go 1.14 + +require github.com/hyperledger/fabric-contract-api-go v1.1.0 diff --git a/token-account-based/chaincode-go/go.sum b/token-account-based/chaincode-go/go.sum new file mode 100644 index 00000000..5a92905b --- /dev/null +++ b/token-account-based/chaincode-go/go.sum @@ -0,0 +1,145 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/DATA-DOG/go-txdb v0.1.3/go.mod h1:DhAhxMXZpUJVGnT+p9IbzJoRKvlArO2pkHjnGX7o0n0= +github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/cucumber/godog v0.8.0/go.mod h1:Cp3tEV1LRAyH/RuCThcxHS/+9ORZ+FMzPva2AZ5Ki+A= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= +github.com/go-openapi/jsonpointer v0.19.3 h1:gihV7YNZK1iK6Tgwwsxo2rJbD1GTbdm72325Bq8FI3w= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.19.2 h1:o20suLFB4Ri0tuzpWtyHlh7E7HnkqTNLq6aR6WVNS1w= +github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= +github.com/go-openapi/spec v0.19.4 h1:ixzUSnHTd6hCemgtAJgluaTSGYpLNpJY4mA2DIkdOAo= +github.com/go-openapi/spec v0.19.4/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= +github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/gobuffalo/envy v1.7.0 h1:GlXgaiBkmrYMHco6t4j7SacKO4XUjvh5pwXh0f4uxXU= +github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= +github.com/gobuffalo/logger v1.0.0/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs= +github.com/gobuffalo/packd v0.3.0 h1:eMwymTkA1uXsqxS0Tpoop3Lc0u3kTfiMBE6nKtQU4g4= +github.com/gobuffalo/packd v0.3.0/go.mod h1:zC7QkmNkYVGKPw4tHpBQ+ml7W/3tIebgeo1b36chA3Q= +github.com/gobuffalo/packr v1.30.1 h1:hu1fuVR3fXEZR7rXNW3h8rqSML8EVAf6KNm0NKO/wKg= +github.com/gobuffalo/packr v1.30.1/go.mod h1:ljMyFO2EcrnzsHsN99cvbq055Y9OhRrIaviy289eRuk= +github.com/gobuffalo/packr/v2 v2.5.1/go.mod h1:8f9c96ITobJlPzI44jj+4tHnEKNt0xXWSVlXRN9X1Iw= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hyperledger/fabric-chaincode-go v0.0.0-20200424173110-d7076418f212 h1:1i4lnpV8BDgKOLi1hgElfBqdHXjXieSuj8629mwBZ8o= +github.com/hyperledger/fabric-chaincode-go v0.0.0-20200424173110-d7076418f212/go.mod h1:N7H3sA7Tx4k/YzFq7U0EPdqJtqvM4Kild0JoCc7C0Dc= +github.com/hyperledger/fabric-contract-api-go v1.1.0 h1:K9uucl/6eX3NF0/b+CGIiO1IPm1VYQxBkpnVGJur2S4= +github.com/hyperledger/fabric-contract-api-go v1.1.0/go.mod h1:nHWt0B45fK53owcFpLtAe8DH0Q5P068mnzkNXMPSL7E= +github.com/hyperledger/fabric-protos-go v0.0.0-20190919234611-2a87503ac7c9/go.mod h1:xVYTjK4DtZRBxZ2D9aE4y6AbLaPwue2o/criQyQbVD0= +github.com/hyperledger/fabric-protos-go v0.0.0-20200424173316-dd554ba3746e h1:9PS5iezHk/j7XriSlNuSQILyCOfcZ9wZ3/PiucmSE8E= +github.com/hyperledger/fabric-protos-go v0.0.0-20200424173316-dd554ba3746e/go.mod h1:xVYTjK4DtZRBxZ2D9aE4y6AbLaPwue2o/criQyQbVD0= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= +github.com/karrick/godirwalk v1.10.12/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e h1:hB2xlXdHp/pmPZq0y3QnmWAArdw9PqbmotexnWx/FU8= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.3.0 h1:RR9dF3JtopPvtkroDZuVD7qquD0bnHlKSqaQhgwt8yk= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297 h1:k7pJ2yAPLPgbskkFdhRCsA77k2fySZ1zf2zCjvQCiIM= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190710143415-6ec70d6a5542 h1:6ZQFf1D2YYDDI7eSwW8adlkkavTB9sw5I24FVtEvNUQ= +golang.org/x/sys v0.0.0-20190710143415-6ec70d6a5542/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190624180213-70d37148ca0c/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20180831171423-11092d34479b h1:lohp5blsw53GBXtLyLNaTXPXS9pJ1tiTw61ZHUoE9Qw= +google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/grpc v1.23.0 h1:AzbTB6ux+okLTzP8Ru1Xs41C303zdcfEht7MQnYJt5A= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/token-account-based/chaincode-go/token_account_based.go b/token-account-based/chaincode-go/token_account_based.go new file mode 100644 index 00000000..0174084d --- /dev/null +++ b/token-account-based/chaincode-go/token_account_based.go @@ -0,0 +1,23 @@ +/* +SPDX-License-Identifier: Apache-2.0 +*/ + +package main + +import ( + "log" + + "github.com/hyperledger/fabric-contract-api-go/contractapi" + "github.com/hyperledger/fabric-samples/token-account-based/chaincode-go/chaincode" +) + +func main() { + tokenChaincode, err := contractapi.NewChaincode(&chaincode.SmartContract{}) + if err != nil { + log.Panicf("Error creating token-account-based chaincode: %v", err) + } + + if err := tokenChaincode.Start(); err != nil { + log.Panicf("Error starting token-account-based chaincode: %v", err) + } +} From 882218af0f7e194c9f158a03fcc018f6e7563bcd Mon Sep 17 00:00:00 2001 From: denyeart Date: Wed, 14 Oct 2020 02:58:24 -0400 Subject: [PATCH 14/38] Add UTXO token sample (go chaincode) (#319) Add a UTXO token sample go chaincode, as well as an associated readme tutorial. Signed-off-by: David Enyeart --- token-utxo/README.md | 212 +++++++++++++++++ .../chaincode-go/chaincode/token_contract.go | 217 ++++++++++++++++++ token-utxo/chaincode-go/go.mod | 5 + token-utxo/chaincode-go/go.sum | 145 ++++++++++++ token-utxo/chaincode-go/token_utxo.go | 23 ++ 5 files changed, 602 insertions(+) create mode 100644 token-utxo/README.md create mode 100644 token-utxo/chaincode-go/chaincode/token_contract.go create mode 100644 token-utxo/chaincode-go/go.mod create mode 100644 token-utxo/chaincode-go/go.sum create mode 100644 token-utxo/chaincode-go/token_utxo.go diff --git a/token-utxo/README.md b/token-utxo/README.md new file mode 100644 index 00000000..6fafd1c1 --- /dev/null +++ b/token-utxo/README.md @@ -0,0 +1,212 @@ +# UTXO token scenario + +The UTXO token smart contract demonstrates how to create and transfer fungible tokens using a UTXO (unspent transaction output) model. In a UTXO model, unspent transaction outputs representing a number of tokens can be 'spent' to transfer tokens between participants. +An unspent transaction output can only be spent once, and the full value must be completely spent. A transaction that spends UTXOs as input will also generate new UTXOs as outputs, where the value of the inputs must equal the value of the outputs. As an example, if you own an unspent transaction output representing 5000 tokens, and you need to transfer 100 tokens to a recipient, the transaction would spend the 5000 token UTXO as input, create a new 100 token UTXO output owned by the recipient, and return a new 4900 token UTXO to you as 'change'. + +Each UTXO in this sample has a key derived from the transaction id that created it, as well as a number of tokens, and an owner that is authorized to spend the tokens. +Ownership of each UTXO could be represented at the organization level or client identity level. In this sample UTXO ownership is based on a client identity, where the client ID is simply a base64-encoded concatenation of the issuer and subject from the client identity's enrollment certificate. The client ID can therefore be used as the payment address when transferring tokens in a UTXO transaction. + +While a transfer transaction spends UTXOs and creates new UTXOs for the recipient(s), a mint transaction can create new UTXOs. In this sample it is assumed that only one organization (played by Org1) is in a central banker role and can mint new tokens owned by their client ID. Any client from any organization can transfer tokens in a UTXO transaction. + +In this tutorial, you will mint and transfer tokens as follows: + +- A member of Org1 uses the `Mint` function to create a UTXO representing a number of tokens. The `Mint` function reads the certificate information of the client identity that submitted the transaction using the `GetClientIdentity.GetID()` API and assigns the UTXO ownership to the minter client ID. +- The same minter client will then use the `Transfer` function to transfer the requested number of tokens to a recipient. The minted UTXO key gets passed as input to the `Transfer` function, a UTXO output representing the number of transferred tokens gets created for the recipient, and another UTXO output representing the 'change' gets created for the minter. It is assumed that the recipient has provided their client ID to the transfer caller out of band. The recipient can then transfer tokens to other registered users in the same fashion. + +## Bring up the test network + +You can run the UTXO token transfer scenario using the Fabric test network. Open a command terminal and navigate to the test network directory in your local clone of the `fabric-samples`. We will operate from the `test-network` directory for the remainder of the tutorial. +``` +cd fabric-samples/test-network +``` + +Run the following command to start the test network: +``` +./network.sh up createChannel -ca +``` + +The test network is deployed with two peer organizations. The `createChannel` flag deploys the network with a single channel named `mychannel` with Org1 and Org2 as channel members. +The -ca flag is used to deploy the network using certificate authorities. This allows you to use each organization's CA to register and enroll new users for this tutorial. + +## Deploy the smart contract to the channel + +You can use the test network script to deploy the UTXO token contract to the channel that was just created. Deploy the smart contract to `mychannel` using the following command: +``` +./network.sh deployCC -ccn token_utxo -ccp ../token-utxo/chaincode-go/ +``` + +The above command deploys the go chaincode with short name `token_utxo`. The smart contract will utilize the default endorsement policy of majority of channel members. +Since the channel has two members, this implies that we'll need to get peer endorsements from 2 out of the 2 channel members. + +Now you are ready to call the deployed smart contract via peer CLI calls. But let's first create the client identities for our scenario. + +## Register identities + +The smart contract supports UTXO ownership based on individual client identities from organizations that are members of the channel. In our scenario, the minter of the tokens will be a member of Org1, while the recipient will belong to Org2. To highlight the connection between the `GetClientIdentity().GetID()` API and the information within a user's certificate, we will register two new identities using the Org1 and Org2 Certificate Authorities (CA's), and then use the CA's to generate each identity's certificate and private key. + +First, we need to set the following environment variables to use the the Fabric CA client (and subsequent commands) +``` +export PATH=${PWD}/../bin:${PWD}:$PATH +export FABRIC_CFG_PATH=$PWD/../config/ +``` + +The terminal we have been using will represent Org1. We will use the Org1 CA to create the minter identity. Set the Fabric CA client home to the MSP of the Org1 CA admin (this identity was generated by the test network script): +``` +export FABRIC_CA_CLIENT_HOME=${PWD}/organizations/peerOrganizations/org1.example.com/ +``` + +You can register a new minter client identity using the `fabric-ca-client` tool: +``` +fabric-ca-client register --caname ca-org1 --id.name minter --id.secret minterpw --id.type client --tls.certfiles ${PWD}/organizations/fabric-ca/org1/tls-cert.pem +``` + +You can now generate the identity certificates and MSP folder by providing the enroll name and secret to the enroll command: +``` +fabric-ca-client enroll -u https://minter:minterpw@localhost:7054 --caname ca-org1 -M ${PWD}/organizations/peerOrganizations/org1.example.com/users/minter@org1.example.com/msp --tls.certfiles ${PWD}/organizations/fabric-ca/org1/tls-cert.pem +``` + +Run the command below to copy the Node OU configuration file into the minter identity MSP folder. +``` +cp ${PWD}/organizations/peerOrganizations/org1.example.com/msp/config.yaml ${PWD}/organizations/peerOrganizations/org1.example.com/users/minter@org1.example.com/msp/config.yaml +``` + +Open a new terminal to represent Org2 and navigate to /fabric-samples/test-network. We'll use the Org2 CA to create the Org2 recipient identity. Set the Fabric CA client home to the MSP of the Org2 CA admin: +``` +cd fabric-samples/test-network +export FABRIC_CA_CLIENT_HOME=${PWD}/organizations/peerOrganizations/org2.example.com/ +``` + +You can register a recipient client identity using the `fabric-ca-client` tool: +``` +fabric-ca-client register --caname ca-org2 --id.name recipient --id.secret recipientpw --id.type client --tls.certfiles ${PWD}/organizations/fabric-ca/org2/tls-cert.pem +``` + +We can now enroll to generate the identity certificates and MSP folder: +``` +fabric-ca-client enroll -u https://recipient:recipientpw@localhost:8054 --caname ca-org2 -M ${PWD}/organizations/peerOrganizations/org2.example.com/users/recipient@org2.example.com/msp --tls.certfiles ${PWD}/organizations/fabric-ca/org2/tls-cert.pem +``` + +Run the command below to copy the Node OU configuration file into the recipient identity MSP folder. +``` +cp ${PWD}/organizations/peerOrganizations/org2.example.com/msp/config.yaml ${PWD}/organizations/peerOrganizations/org2.example.com/users/recipient@org2.example.com/msp/config.yaml +``` + +## Mint some tokens + +Now that we have created the identity of the minter, we can invoke the smart contract to mint some tokens. +Shift back to the Org1 terminal, we'll set the following environment variables to operate the `peer` CLI as the minter identity from Org1. +``` +export CORE_PEER_TLS_ENABLED=true +export CORE_PEER_LOCALMSPID="Org1MSP" +export CORE_PEER_MSPCONFIGPATH=${PWD}/organizations/peerOrganizations/org1.example.com/users/minter@org1.example.com/msp +export CORE_PEER_TLS_ROOTCERT_FILE=${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt +export CORE_PEER_ADDRESS=localhost:7051 +export TARGET_TLS_OPTIONS="-o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem --peerAddresses localhost:7051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt --peerAddresses localhost:9051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt" +``` + +The last environment variable above will be utilized within the CLI invoke commands to set the target peers for endorsement, and the target ordering service endpoint and TLS options. + +We can then invoke the smart contract to mint 5000 tokens: +``` +peer chaincode invoke $TARGET_TLS_OPTIONS -C mychannel -n token_utxo -c '{"function":"Mint","Args":["5000"]}' +``` + +The mint function validated that the client is a member of the minter organization, and then created a UTXO with 5000 tokens belonging to the minter client identity. +The function returns the UTXO that was created so that we can inspect it. Here is the returned UTXO with JSON formatting applied: +``` +{ + "utxo_key":"c3706696c537e7bd6940fedfd52f4a3a630d253297db0ecc2b3ba514b45f5e7c.0", + "owner":"eDUwOTo6Q049bWludGVyLE9VPWNsaWVudCxPPUh5cGVybGVkZ2VyLFNUPU5vcnRoIENhcm9saW5hLEM9VVM6OkNOPWNhLm9yZzEuZXhhbXBsZS5jb20sTz1vcmcxLmV4YW1wbGUuY29tLEw9RHVyaGFtLFNUPU5vcnRoIENhcm9saW5hLEM9VVM=", + "amount":5000 +} +``` + +Notice that the utxo_key is set to the transaction id, concatenated with ".0" to indicate that this is the first (and only) UTXO output of the transaction. Your transaction id will be different and unique. The owner is set to the minter's client ID, meaning that only this client can spend the UTXO, and the amount is "5000". + +The utxo_key that was created will be needed when you spend the UTXO in the Transfer function below. Copy the utxo_key value (including the ".0") so that you'll have it available for the Transfer function. + +We can check the minter client's total set of owned UTXOs by calling the `ClientUTXOs` function. +``` +peer chaincode query -C mychannel -n token_utxo -c '{"function":"ClientUTXOs","Args":[]}' +``` + +The same minted UTXO is returned. + + +## Transfer tokens + +The minter intends to transfer 100 tokens to the Org2 recipient, but first the Org2 recipient needs to provide their own client ID as the payment address. +A client can derive their client ID from their own public certificate, but to be sure the client ID is accurate, the contract has a `ClientID` utility function that simply looks at the callers certificate and returns the calling client's ID. +Let's prepare the Org2 terminal by setting the environment variables for the Org2 recipient user. +``` +export PATH=${PWD}/../bin:${PWD}:$PATH +export FABRIC_CFG_PATH=$PWD/../config/ +export CORE_PEER_TLS_ENABLED=true +export CORE_PEER_LOCALMSPID="Org2MSP" +export CORE_PEER_MSPCONFIGPATH=${PWD}/organizations/peerOrganizations/org2.example.com/users/recipient@org2.example.com/msp +export CORE_PEER_TLS_ROOTCERT_FILE=${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt +export CORE_PEER_ADDRESS=localhost:9051 +``` + +Using the Org2 terminal, the Org2 recipient user can retrieve their own client ID: +``` +peer chaincode query -C mychannel -n token_utxo -c '{"function":"ClientID","Args":[]}' +``` + +The function returns of recipient's client ID: +``` +eDUwOTo6Q049cmVjaXBpZW50LE9VPWNsaWVudCxPPUh5cGVybGVkZ2VyLFNUPU5vcnRoIENhcm9saW5hLEM9VVM6OkNOPWNhLm9yZzIuZXhhbXBsZS5jb20sTz1vcmcyLmV4YW1wbGUuY29tLEw9SHVyc2xleSxTVD1IYW1wc2hpcmUsQz1VSw== +``` + +Let's base64 decode the client ID to make sure it is the Org2 recipient user: +``` +echo eDUwOTo6Q049cmVjaXBpZW50LE9VPWNsaWVudCxPPUh5cGVybGVkZ2VyLFNUPU5vcnRoIENhcm9saW5hLEM9VVM6OkNOPWNhLm9yZzIuZXhhbXBsZS5jb20sTz1vcmcyLmV4YW1wbGUuY29tLEw9SHVyc2xleSxTVD1IYW1wc2hpcmUsQz1VSw== | base64 --decode +``` + +The result shows that the subject and issuer is indeed the recipient user from Org2: +``` +x509::CN=recipient,OU=client,O=Hyperledger,ST=North Carolina,C=US::CN=ca.org2.example.com,O=org2.example.com,L=Hursley,ST=Hampshire,C=UK +``` + +After the Org2 recipient provides their client ID to the minter, the minter can initiate a transfer of tokens. We'll pass in the utxo_key of the UTXO with 5000 tokens to spend, and request that two new UTXOs get created, a UTXO with 100 tokens for the recipient, and a UTXO with 4900 tokens for the minter as the 'change'. Since the contract will create the UTXO output keys, we'll initially leave the output keys blank. +Back in the Org1 terminal, request the UTXO transfer. **Replace YOUR_UTXO_KEY below with the key you saved earlier**: +``` +peer chaincode invoke $TARGET_TLS_OPTIONS -C mychannel -n token_utxo -c '{"function":"Transfer","Args":["[\"YOUR_UTXO_KEY\"]"," [{\"utxo_key\":\"\",\"owner\":\"eDUwOTo6Q049cmVjaXBpZW50LE9VPWNsaWVudCxPPUh5cGVybGVkZ2VyLFNUPU5vcnRoIENhcm9saW5hLEM9VVM6OkNOPWNhLm9yZzIuZXhhbXBsZS5jb20sTz1vcmcyLmV4YW1wbGUuY29tLEw9SHVyc2xleSxTVD1IYW1wc2hpcmUsQz1VSw==\",\"amount\":100},{\"utxo_key\":\"\",\"owner\":\"eDUwOTo6Q049bWludGVyLE9VPWNsaWVudCxPPUh5cGVybGVkZ2VyLFNUPU5vcnRoIENhcm9saW5hLEM9VVM6OkNOPWNhLm9yZzEuZXhhbXBsZS5jb20sTz1vcmcxLmV4YW1wbGUuY29tLEw9RHVyaGFtLFNUPU5vcnRoIENhcm9saW5hLEM9VVM=\",\"amount\":4900}]"]}' +``` + +The `Transfer` function verifies that the calling client owns the input UTXO, and that the sum of the input amounts equals the sum of the output amounts. It will then delete (spend) the input UTXO, and create the two output UTXOs. If you passed the incorrect UTXO input key, or requested UTXO output values that don't total 5000, you'll get an error indicating as such. + +The new UTXO outputs are returned in the successful response: +``` +[{\"utxo_key\":\"e51c3d19e92326f772e49e8a3e58f2bbf72bc3905e55fcd649b97a91b9b2cf44.0\",\"owner\":\"eDUwOTo6Q049cmVjaXBpZW50LE9VPWNsaWVudCxPPUh5cGVybGVkZ2VyLFNUPU5vcnRoIENhcm9saW5hLEM9VVM6OkNOPWNhLm9yZzIuZXhhbXBsZS5jb20sTz1vcmcyLmV4YW1wbGUuY29tLEw9SHVyc2xleSxTVD1IYW1wc2hpcmUsQz1VSw==\",\"amount\":100},{\"utxo_key\":\"e51c3d19e92326f772e49e8a3e58f2bbf72bc3905e55fcd649b97a91b9b2cf44.1\",\"owner\":\"eDUwOTo6Q049bWludGVyLE9VPWNsaWVudCxPPUh5cGVybGVkZ2VyLFNUPU5vcnRoIENhcm9saW5hLEM9VVM6OkNOPWNhLm9yZzEuZXhhbXBsZS5jb20sTz1vcmcxLmV4YW1wbGUuY29tLEw9RHVyaGFtLFNUPU5vcnRoIENhcm9saW5hLEM9VVM=\",\"amount\":4900}] +``` + +While still in the Org1 terminal, let's request the minter's UTXOs again: +``` +peer chaincode query -C mychannel -n token_utxo -c '{"function":"ClientUTXOs","Args":[]}' +``` + +The new UTXO worth 4900 tokens is returned. + +And then using the Org2 terminal, let's request the recipient's UTXOs: +``` +peer chaincode query -C mychannel -n token_utxo -c '{"function":"ClientUTXOs","Args":[]}' +``` + +The new UTXO worth 100 tokens is returned. + +Congratulations, you've transferred 100 tokens! The Org2 recipient can now transfer tokens to other registered users in the same manner. + +## Clean up + +When you are finished, you can bring down the test network. The command will remove all the nodes of the test network, and delete any ledger data that you created: +``` +./network.sh down +``` + +## Contract extension ideas + +You can extend the basic UTXO token sample to meet other requirements. For example: + +* Rather than using the default 'majority' endorsement policy, you could set the endorsement policy to a subset of organizations that represent trust anchors for the contract execution. +* You could utilize anonymous payment addresses on the UTXO outputs based on private-public key pairs, instead of UTXOs assigned to a client ID. In order to spend the tokens, the client would have to sign the transfer input as proof that they own the address private key, which the contract would then validate, similar to the Bitcoin model in the permissionless blockchain space. However, in a permissioned blockchain such as Fabric, only registered clients are authorized to participate. Furthermore, if you don't want to leak the registered client identity associated with each address, the clients could be registered using an Identity Mixer MSP, so that the client itself is also anonymous in each of the token transactions. diff --git a/token-utxo/chaincode-go/chaincode/token_contract.go b/token-utxo/chaincode-go/chaincode/token_contract.go new file mode 100644 index 00000000..cc96b309 --- /dev/null +++ b/token-utxo/chaincode-go/chaincode/token_contract.go @@ -0,0 +1,217 @@ +package chaincode + +import ( + "fmt" + "log" + "strconv" + + "github.com/hyperledger/fabric-contract-api-go/contractapi" +) + +// SmartContract provides functions for transferring tokens using UTXO transactions +type SmartContract struct { + contractapi.Contract +} + +// UTXO represents an unspent transaction output +type UTXO struct { + Key string `json:"utxo_key"` + Owner string `json:"owner"` + Amount int `json:"amount"` +} + +// Mint creates a new unspent transaction output (UTXO) owned by the minter +func (s *SmartContract) Mint(ctx contractapi.TransactionContextInterface, amount int) (*UTXO, error) { + + // Check minter authorization - this sample assumes Org1 is the central banker with privilege to mint new tokens + clientMSPID, err := ctx.GetClientIdentity().GetMSPID() + if err != nil { + return nil, fmt.Errorf("failed to get MSPID: %v", err) + } + if clientMSPID != "Org1MSP" { + return nil, fmt.Errorf("client is not authorized to mint new tokens") + } + + // Get ID of submitting client identity + minter, err := ctx.GetClientIdentity().GetID() + if err != nil { + return nil, fmt.Errorf("failed to get client id: %v", err) + } + + if amount <= 0 { + return nil, fmt.Errorf("mint amount must be a positive integer") + } + + utxo := UTXO{} + utxo.Key = ctx.GetStub().GetTxID() + ".0" + utxo.Owner = minter + utxo.Amount = amount + + // the utxo has a composite key of owner:utxoKey, this enables ClientUTXOs() function to query for an owner's utxos. + utxoCompositeKey, err := ctx.GetStub().CreateCompositeKey("utxo", []string{minter, utxo.Key}) + + err = ctx.GetStub().PutState(utxoCompositeKey, []byte(strconv.Itoa(amount))) + if err != nil { + return nil, err + } + + log.Printf("utxo minted: %+v", utxo) + + return &utxo, nil +} + +// Transfer transfers UTXOs containing tokens from client to recipient(s) +func (s *SmartContract) Transfer(ctx contractapi.TransactionContextInterface, utxoInputKeys []string, utxoOutputs []UTXO) ([]UTXO, error) { + + // Get ID of submitting client identity + clientID, err := ctx.GetClientIdentity().GetID() + if err != nil { + return nil, fmt.Errorf("failed to get client id: %v", err) + } + + // Validate and summarize utxo inputs + var utxoInputs []*UTXO + var totalInputAmount int + for _, utxoInputKey := range utxoInputKeys { + utxoInputCompositeKey, err := ctx.GetStub().CreateCompositeKey("utxo", []string{clientID, utxoInputKey}) + if err != nil { + return nil, fmt.Errorf("failed to create composite key: %v", err) + } + + // validate that client has a utxo matching the input key + valueBytes, err := ctx.GetStub().GetState(utxoInputCompositeKey) + if err != nil { + return nil, fmt.Errorf("failed to read utxoInputCompositeKey %s from world state: %v", utxoInputCompositeKey, err) + } + + if valueBytes == nil { + return nil, fmt.Errorf("utxoInput %s not found for client %s", utxoInputKey, clientID) + } + + amount, _ := strconv.Atoi(string(valueBytes)) // Error handling not needed since Itoa() was used when setting the utxo amount, guaranteeing it was an integer. + + utxoInput := &UTXO{ + Key: utxoInputKey, + Owner: clientID, + Amount: amount, + } + + totalInputAmount += amount + utxoInputs = append(utxoInputs, utxoInput) + } + + // Validate and summarize utxo outputs + var totalOutputAmount int + txID := ctx.GetStub().GetTxID() + for i, utxoOutput := range utxoOutputs { + + if utxoOutput.Amount <= 0 { + return nil, fmt.Errorf("utxo output amount must be a positive integer") + } + + utxoOutputs[i].Key = fmt.Sprintf("%s.%d", txID, i) + + totalOutputAmount += utxoOutput.Amount + } + + // Validate total inputs equals total outputs + if totalInputAmount != totalOutputAmount { + return nil, fmt.Errorf("total utxoInput amount %d does not equal total utxoOutput amount %d", totalInputAmount, totalOutputAmount) + } + + // Since the transaction is valid, now delete utxo inputs from owner's state + for _, utxoInput := range utxoInputs { + + utxoInputCompositeKey, err := ctx.GetStub().CreateCompositeKey("utxo", []string{utxoInput.Owner, utxoInput.Key}) + if err != nil { + return nil, fmt.Errorf("failed to create composite key: %v", err) + } + + err = ctx.GetStub().DelState(utxoInputCompositeKey) + if err != nil { + return nil, err + } + log.Printf("utxoInput deleted: %+v", utxoInput) + } + + // Create utxo outputs using a composite key based on the owner and utxo key + for _, utxoOutput := range utxoOutputs { + utxoOutputCompositeKey, err := ctx.GetStub().CreateCompositeKey("utxo", []string{utxoOutput.Owner, utxoOutput.Key}) + if err != nil { + return nil, fmt.Errorf("failed to create composite key: %v", err) + } + + err = ctx.GetStub().PutState(utxoOutputCompositeKey, []byte(strconv.Itoa(utxoOutput.Amount))) + if err != nil { + return nil, err + } + log.Printf("utxoOutput created: %+v", utxoOutput) + } + + return utxoOutputs, nil +} + +// ClientUTXOs returns all UTXOs owned by the calling client +func (s *SmartContract) ClientUTXOs(ctx contractapi.TransactionContextInterface) ([]*UTXO, error) { + + // Get ID of submitting client identity + clientID, err := ctx.GetClientIdentity().GetID() + if err != nil { + return nil, fmt.Errorf("failed to get client id: %v", err) + } + + // since utxos have a composite key of owner:utxoKey, we can query for all utxos matching owner:* + utxoResultsIterator, err := ctx.GetStub().GetStateByPartialCompositeKey("utxo", []string{clientID}) + if err != nil { + return nil, err + } + defer utxoResultsIterator.Close() + + var utxos []*UTXO + for utxoResultsIterator.HasNext() { + utxoRecord, err := utxoResultsIterator.Next() + if err != nil { + return nil, err + } + + // composite key is expected to be owner:utxoKey + _, compositeKeyParts, err := ctx.GetStub().SplitCompositeKey(utxoRecord.Key) + if err != nil { + return nil, err + } + + if len(compositeKeyParts) != 2 { + return nil, fmt.Errorf("expected composite key with two parts (owner:utxoKey)") + } + + utxoKey := compositeKeyParts[1] // owner is at [0], utxoKey is at[1] + + if utxoRecord.Value == nil { + return nil, fmt.Errorf("utxo %s has no value", utxoKey) + } + + amount, _ := strconv.Atoi(string(utxoRecord.Value)) // Error handling not needed since Itoa() was used when setting the utxo amount, guaranteeing it was an integer. + + utxo := &UTXO{ + Key: utxoKey, + Owner: clientID, + Amount: amount, + } + + utxos = append(utxos, utxo) + } + return utxos, nil +} + +// ClientID returns the client id of the calling client +// Users can use this function to get their own client id, which they can then give to others as the payment address +func (s *SmartContract) ClientID(ctx contractapi.TransactionContextInterface) (string, error) { + + // Get ID of submitting client identity + clientID, err := ctx.GetClientIdentity().GetID() + if err != nil { + return "", fmt.Errorf("failed to get client id: %v", err) + } + + return clientID, nil +} diff --git a/token-utxo/chaincode-go/go.mod b/token-utxo/chaincode-go/go.mod new file mode 100644 index 00000000..9cdc693f --- /dev/null +++ b/token-utxo/chaincode-go/go.mod @@ -0,0 +1,5 @@ +module github.com/hyperledger/fabric-samples/token-utxo/chaincode-go + +go 1.14 + +require github.com/hyperledger/fabric-contract-api-go v1.1.0 diff --git a/token-utxo/chaincode-go/go.sum b/token-utxo/chaincode-go/go.sum new file mode 100644 index 00000000..5a92905b --- /dev/null +++ b/token-utxo/chaincode-go/go.sum @@ -0,0 +1,145 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/DATA-DOG/go-txdb v0.1.3/go.mod h1:DhAhxMXZpUJVGnT+p9IbzJoRKvlArO2pkHjnGX7o0n0= +github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/cucumber/godog v0.8.0/go.mod h1:Cp3tEV1LRAyH/RuCThcxHS/+9ORZ+FMzPva2AZ5Ki+A= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= +github.com/go-openapi/jsonpointer v0.19.3 h1:gihV7YNZK1iK6Tgwwsxo2rJbD1GTbdm72325Bq8FI3w= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.19.2 h1:o20suLFB4Ri0tuzpWtyHlh7E7HnkqTNLq6aR6WVNS1w= +github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= +github.com/go-openapi/spec v0.19.4 h1:ixzUSnHTd6hCemgtAJgluaTSGYpLNpJY4mA2DIkdOAo= +github.com/go-openapi/spec v0.19.4/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= +github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/gobuffalo/envy v1.7.0 h1:GlXgaiBkmrYMHco6t4j7SacKO4XUjvh5pwXh0f4uxXU= +github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= +github.com/gobuffalo/logger v1.0.0/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs= +github.com/gobuffalo/packd v0.3.0 h1:eMwymTkA1uXsqxS0Tpoop3Lc0u3kTfiMBE6nKtQU4g4= +github.com/gobuffalo/packd v0.3.0/go.mod h1:zC7QkmNkYVGKPw4tHpBQ+ml7W/3tIebgeo1b36chA3Q= +github.com/gobuffalo/packr v1.30.1 h1:hu1fuVR3fXEZR7rXNW3h8rqSML8EVAf6KNm0NKO/wKg= +github.com/gobuffalo/packr v1.30.1/go.mod h1:ljMyFO2EcrnzsHsN99cvbq055Y9OhRrIaviy289eRuk= +github.com/gobuffalo/packr/v2 v2.5.1/go.mod h1:8f9c96ITobJlPzI44jj+4tHnEKNt0xXWSVlXRN9X1Iw= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hyperledger/fabric-chaincode-go v0.0.0-20200424173110-d7076418f212 h1:1i4lnpV8BDgKOLi1hgElfBqdHXjXieSuj8629mwBZ8o= +github.com/hyperledger/fabric-chaincode-go v0.0.0-20200424173110-d7076418f212/go.mod h1:N7H3sA7Tx4k/YzFq7U0EPdqJtqvM4Kild0JoCc7C0Dc= +github.com/hyperledger/fabric-contract-api-go v1.1.0 h1:K9uucl/6eX3NF0/b+CGIiO1IPm1VYQxBkpnVGJur2S4= +github.com/hyperledger/fabric-contract-api-go v1.1.0/go.mod h1:nHWt0B45fK53owcFpLtAe8DH0Q5P068mnzkNXMPSL7E= +github.com/hyperledger/fabric-protos-go v0.0.0-20190919234611-2a87503ac7c9/go.mod h1:xVYTjK4DtZRBxZ2D9aE4y6AbLaPwue2o/criQyQbVD0= +github.com/hyperledger/fabric-protos-go v0.0.0-20200424173316-dd554ba3746e h1:9PS5iezHk/j7XriSlNuSQILyCOfcZ9wZ3/PiucmSE8E= +github.com/hyperledger/fabric-protos-go v0.0.0-20200424173316-dd554ba3746e/go.mod h1:xVYTjK4DtZRBxZ2D9aE4y6AbLaPwue2o/criQyQbVD0= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= +github.com/karrick/godirwalk v1.10.12/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e h1:hB2xlXdHp/pmPZq0y3QnmWAArdw9PqbmotexnWx/FU8= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.3.0 h1:RR9dF3JtopPvtkroDZuVD7qquD0bnHlKSqaQhgwt8yk= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297 h1:k7pJ2yAPLPgbskkFdhRCsA77k2fySZ1zf2zCjvQCiIM= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190710143415-6ec70d6a5542 h1:6ZQFf1D2YYDDI7eSwW8adlkkavTB9sw5I24FVtEvNUQ= +golang.org/x/sys v0.0.0-20190710143415-6ec70d6a5542/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190624180213-70d37148ca0c/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20180831171423-11092d34479b h1:lohp5blsw53GBXtLyLNaTXPXS9pJ1tiTw61ZHUoE9Qw= +google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/grpc v1.23.0 h1:AzbTB6ux+okLTzP8Ru1Xs41C303zdcfEht7MQnYJt5A= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/token-utxo/chaincode-go/token_utxo.go b/token-utxo/chaincode-go/token_utxo.go new file mode 100644 index 00000000..481a6d30 --- /dev/null +++ b/token-utxo/chaincode-go/token_utxo.go @@ -0,0 +1,23 @@ +/* +SPDX-License-Identifier: Apache-2.0 +*/ + +package main + +import ( + "log" + + "github.com/hyperledger/fabric-contract-api-go/contractapi" + "github.com/hyperledger/fabric-samples/token-utxo/chaincode-go/chaincode" +) + +func main() { + tokenChaincode, err := contractapi.NewChaincode(&chaincode.SmartContract{}) + if err != nil { + log.Panicf("Error creating token-utxo chaincode: %v", err) + } + + if err := tokenChaincode.Start(); err != nil { + log.Panicf("Error starting token-utxo chaincode: %v", err) + } +} From cb42df6d7ac798c536ea1d96581bf320587e8e57 Mon Sep 17 00:00:00 2001 From: Arnaud J Le Hors Date: Wed, 14 Oct 2020 09:00:22 +0200 Subject: [PATCH 15/38] Fix token-utxo-based README terminal 2 setup The user's PATH must be set on the first use of Terminal 2 for the call to the fabric-ca-client command to succeed. The current README only sets it later. This fix changes that. Signed-off-by: Arnaud J Le Hors --- token-utxo/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/token-utxo/README.md b/token-utxo/README.md index 6fafd1c1..665d9807 100644 --- a/token-utxo/README.md +++ b/token-utxo/README.md @@ -70,9 +70,10 @@ Run the command below to copy the Node OU configuration file into the minter ide cp ${PWD}/organizations/peerOrganizations/org1.example.com/msp/config.yaml ${PWD}/organizations/peerOrganizations/org1.example.com/users/minter@org1.example.com/msp/config.yaml ``` -Open a new terminal to represent Org2 and navigate to /fabric-samples/test-network. We'll use the Org2 CA to create the Org2 recipient identity. Set the Fabric CA client home to the MSP of the Org2 CA admin: +Open a new terminal to represent Org2 and navigate to fabric-samples/test-network. We'll use the Org2 CA to create the Org2 recipient identity. Set the Fabric CA client home to the MSP of the Org2 CA admin: ``` cd fabric-samples/test-network +export PATH=${PWD}/../bin:${PWD}:$PATH export FABRIC_CA_CLIENT_HOME=${PWD}/organizations/peerOrganizations/org2.example.com/ ``` @@ -139,7 +140,6 @@ The minter intends to transfer 100 tokens to the Org2 recipient, but first the O A client can derive their client ID from their own public certificate, but to be sure the client ID is accurate, the contract has a `ClientID` utility function that simply looks at the callers certificate and returns the calling client's ID. Let's prepare the Org2 terminal by setting the environment variables for the Org2 recipient user. ``` -export PATH=${PWD}/../bin:${PWD}:$PATH export FABRIC_CFG_PATH=$PWD/../config/ export CORE_PEER_TLS_ENABLED=true export CORE_PEER_LOCALMSPID="Org2MSP" From 949bd9095bcdf6eda47159d15cb278e9d0d4d95f Mon Sep 17 00:00:00 2001 From: Arnaud J Le Hors Date: Wed, 14 Oct 2020 08:37:23 +0200 Subject: [PATCH 16/38] Fix token-account-based README terminal 2 setup The user's PATH must be set on the first use of Terminal 2 for the call to the fabric-ca-client command to succeed. The current README only sets it later. This fix changes that. Signed-off-by: Arnaud J Le Hors --- token-account-based/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/token-account-based/README.md b/token-account-based/README.md index 307b82f6..bceb976a 100644 --- a/token-account-based/README.md +++ b/token-account-based/README.md @@ -69,9 +69,10 @@ Run the command below to copy the Node OU configuration file into the minter ide cp ${PWD}/organizations/peerOrganizations/org1.example.com/msp/config.yaml ${PWD}/organizations/peerOrganizations/org1.example.com/users/minter@org1.example.com/msp/config.yaml ``` -Open a new terminal to represent Org2 and navigate to /fabric-samples/test-network. We'll use the Org2 CA to create the Org2 recipient identity. Set the Fabric CA client home to the MSP of the Org2 CA admin: +Open a new terminal to represent Org2 and navigate to fabric-samples/test-network. We'll use the Org2 CA to create the Org2 recipient identity. Set the Fabric CA client home to the MSP of the Org2 CA admin: ``` cd fabric-samples/test-network +export PATH=${PWD}/../bin:${PWD}:$PATH export FABRIC_CA_CLIENT_HOME=${PWD}/organizations/peerOrganizations/org2.example.com/ ``` @@ -126,7 +127,6 @@ The minter intends to transfer 100 tokens to the Org2 recipient, but first the O A client can derive their account ID from their own public certificate, but to be sure the account ID is accurate, the contract has a `ClientAccountID` utility function that simply looks at the callers certificate and returns the calling client's ID, which will be used as the account ID. Let's prepare the Org2 terminal by setting the environment variables for the Org2 recipient user. ``` -export PATH=${PWD}/../bin:${PWD}:$PATH export FABRIC_CFG_PATH=$PWD/../config/ export CORE_PEER_TLS_ENABLED=true export CORE_PEER_LOCALMSPID="Org2MSP" From d7abc1edeb0c48b709c7b6756fb34a05281f4013 Mon Sep 17 00:00:00 2001 From: Arnaud J Le Hors Date: Wed, 14 Oct 2020 09:21:46 +0200 Subject: [PATCH 17/38] Add Token samples to README Signed-off-by: Arnaud J Le Hors --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index dcf90721..98e29d4a 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,9 @@ Additional samples demonstrate various Fabric use cases and application patterns | -------------|------------------------------|------------------| | [Commercial paper](commercial-paper) | Explore a use case and detailed application development tutorial in which two organizations use a blockchain network to trade commercial paper. | [Commercial paper tutorial](https://hyperledger-fabric.readthedocs.io/en/latest/tutorial/commercial_paper.html) | | [Off chain data](off_chain_data) | Learn how to use the Peer channel-based event services to build an off-chain database for reporting and analytics. | [Peer channel-based event services](https://hyperledger-fabric.readthedocs.io/en/latest/peer_event_services.html) | -| [High throughput](high-throughput) | Learn how you can design your smart contract to avoid transaction collisions in high volume environments. | | +| [Token account-based](token-account-based) | Smart contract demonstrating how to create and transfer fungible tokens using an account-based model. | [README](token-account-based/README.md) | +| [Token UTXO](token-utxo) | Smart contract demonstrating how to create and transfer fungible tokens using a UTXO (unspent transaction output) model. | [README](token-utxo/README.md) | +| [High throughput](high-throughput) | Learn how you can design your smart contract to avoid transaction collisions in high volume environments. | [README](high-throughput/README.md) | | [Chaincode](chaincode) | A set of other sample smart contracts, many of which were used in tutorials prior to the asset transfer sample series. | | | [Interest rate swaps](interest_rate_swaps) | **Deprecated in favor of state based endorsement asset transfer sample** | | | [Fabcar](fabcar) | **Deprecated in favor of basic asset transfer sample** | | From d82adf3e75ef7f9521fe2a8e4775a944701406e8 Mon Sep 17 00:00:00 2001 From: nikhil550 Date: Fri, 16 Oct 2020 11:28:18 -0400 Subject: [PATCH 18/38] Fix go SDK bug and enable go SDK test (#352) Signed-off-by: NIKHIL E GUPTA --- asset-transfer-basic/application-go/go.mod | 2 +- asset-transfer-basic/application-go/go.sum | 150 +++++++++++++++++++++ ci/scripts/run-test-network-basic.sh | 14 +- 3 files changed, 158 insertions(+), 8 deletions(-) diff --git a/asset-transfer-basic/application-go/go.mod b/asset-transfer-basic/application-go/go.mod index 345e094c..92928e17 100644 --- a/asset-transfer-basic/application-go/go.mod +++ b/asset-transfer-basic/application-go/go.mod @@ -2,4 +2,4 @@ module asset-transfer-basic go 1.14 -require github.com/hyperledger/fabric-sdk-go v1.0.0-beta2 +require github.com/hyperledger/fabric-sdk-go v1.0.0-beta3.0.20201006151309-9c426dcc5096 diff --git a/asset-transfer-basic/application-go/go.sum b/asset-transfer-basic/application-go/go.sum index d5ad7d7c..19c5b312 100644 --- a/asset-transfer-basic/application-go/go.sum +++ b/asset-transfer-basic/application-go/go.sum @@ -1,22 +1,46 @@ +bitbucket.org/liamstask/goose v0.0.0-20150115234039-8488cc47d90c/go.mod h1:hSVuE3qU7grINVSwrmzHfpg9k87ALBk+XaualNyUzI4= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0= +github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0= github.com/Knetic/govaluate v3.0.0+incompatible h1:7o6+MAPhYTCF0+fdvoz1xDedhRb4f6s9Tn1Tt7/WTEg= github.com/Knetic/govaluate v3.0.0+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= +github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/certifi/gocertifi v0.0.0-20180118203423-deb3ae2ef261/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudflare/backoff v0.0.0-20161212185259-647f3cdfc87a/go.mod h1:rzgs2ZOiguV6/NpiDgADjRLPNyZlApIWxKpkT+X8SdY= github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004 h1:lkAMpLVBDaj17e85keuznYcH5rqI438v41pKcBl4ZxQ= github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA= +github.com/cloudflare/cfssl v1.4.1 h1:vScfU2DrIUI9VPHBVeeAQ0q5A+9yshO1Gz+3QoUQiKw= +github.com/cloudflare/cfssl v1.4.1/go.mod h1:KManx/OJPb5QY+y0+o/898AMcM128sF0bURvoVUSjTo= +github.com/cloudflare/go-metrics v0.0.0-20151117154305-6a9aea36fb41/go.mod h1:eaZPlJWD+G9wseg1BuRXlHnjntPMrywMsyxf+LTOdP4= +github.com/cloudflare/redoctober v0.0.0-20171127175943-746a508df14c/go.mod h1:6Se34jNoqrd8bTxrmJB2Bg2aoZ2CdSXonils9NsiNgo= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/getsentry/raven-go v0.0.0-20180121060056-563b81fc02b7/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= github.com/go-kit/kit v0.8.0 h1:Wz+5lgoB0kkuqLEc6NVmwRknTKP6dTGbSqvhZtBI/j0= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0 h1:MP4Eh7ZCb31lleYCFuwm0oe4/YGak+5l1vA2NOE80nA= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-sql-driver/mysql v1.3.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= @@ -24,104 +48,230 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfU github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.4.3 h1:GV+pQPG/EUUbkh47niozDcADz6go/dUwhVzdUQHIVRw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/google/certificate-transparency-go v0.0.0-20180222191210-5ab67e519c93 h1:qdfmdGwtm13OVx+AxguOWUTbgmXGn2TbdUHipo3chMg= github.com/google/certificate-transparency-go v0.0.0-20180222191210-5ab67e519c93/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg= +github.com/google/certificate-transparency-go v1.0.21 h1:Yf1aXowfZ2nuboBsg7iYGLmwsOARdV86pfH3g95wXmE= +github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce h1:xdsDDbiBDQTKASoGEZ+pEmF1OnWuu8AQ9I8iNbHNeno= github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/hyperledger/fabric-config v0.0.5 h1:khRkm8U9Ghdg8VmZfptgzCFlCzrka8bPfUkM+/j6Zlg= +github.com/hyperledger/fabric-config v0.0.5/go.mod h1:YpITBI/+ZayA3XWY5lF302K7PAsFYjEEPM/zr3hegA8= github.com/hyperledger/fabric-lib-go v1.0.0 h1:UL1w7c9LvHZUSkIvHTDGklxFv2kTeva1QI2emOVc324= github.com/hyperledger/fabric-lib-go v1.0.0/go.mod h1:H362nMlunurmHwkYqR5uHL2UDWbQdbfz74n8kbCFsqc= github.com/hyperledger/fabric-protos-go v0.0.0-20191121202242-f5500d5e3e85 h1:bNgEcCg5NVRWs/T+VUEfhgh5Olx/N4VB+0+ybW+oSuA= github.com/hyperledger/fabric-protos-go v0.0.0-20191121202242-f5500d5e3e85/go.mod h1:xVYTjK4DtZRBxZ2D9aE4y6AbLaPwue2o/criQyQbVD0= +github.com/hyperledger/fabric-protos-go v0.0.0-20200424173316-dd554ba3746e/go.mod h1:xVYTjK4DtZRBxZ2D9aE4y6AbLaPwue2o/criQyQbVD0= +github.com/hyperledger/fabric-protos-go v0.0.0-20200707132912-fee30f3ccd23 h1:SEbB3yH4ISTGRifDamYXAst36gO2kM855ndMJlsv+pc= +github.com/hyperledger/fabric-protos-go v0.0.0-20200707132912-fee30f3ccd23/go.mod h1:xVYTjK4DtZRBxZ2D9aE4y6AbLaPwue2o/criQyQbVD0= github.com/hyperledger/fabric-sdk-go v1.0.0-beta1.0.20200526155846-219a09aadc0f h1:eAkJx0+8PBbfP6xZxVRD2agk9W7oDbqllxO+ERgnKJk= github.com/hyperledger/fabric-sdk-go v1.0.0-beta1.0.20200526155846-219a09aadc0f/go.mod h1:/s224b8NLvOJOCIqBvWd9O6u7GE33iuIOT6OfcTE1OE= github.com/hyperledger/fabric-sdk-go v1.0.0-beta2 h1:FBYygns0Qga+mQ4PXycyTU5m4N9KAZM+Ttf7agiV7M8= github.com/hyperledger/fabric-sdk-go v1.0.0-beta2/go.mod h1:/s224b8NLvOJOCIqBvWd9O6u7GE33iuIOT6OfcTE1OE= +github.com/hyperledger/fabric-sdk-go v1.0.0-beta3.0.20201006151309-9c426dcc5096 h1:veml7LmfavSHqF8w8z/PGGlfdXvmx5SstQIH6Nyy87c= +github.com/hyperledger/fabric-sdk-go v1.0.0-beta3.0.20201006151309-9c426dcc5096/go.mod h1:qWE9Syfg1KbwNjtILk70bJLilnmCvllIYFCSY/pa1RU= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jmhodges/clock v0.0.0-20160418191101-880ee4c33548/go.mod h1:hGT6jSUVzF6no3QaDSMLGLEHtHSBSefs+MgcDWnmhmo= +github.com/jmoiron/sqlx v0.0.0-20180124204410-05cef0741ade/go.mod h1:IiEW3SEiiErVyFdH8NTuWjSifiEQKUoyK3LNqr2kCHU= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/sqlstruct v0.0.0-20150923205031-648daed35d49/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE= +github.com/kisom/goutils v1.1.0/go.mod h1:+UBTfd78habUYWFbNWTJNG+jNG/i/lGURakr4A/yNRw= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kylelemons/go-gypsy v0.0.0-20160905020020-08cad365cd28/go.mod h1:T/T7jsxVqf9k/zYOqbgNAsANsjxTd1Yq3htjDhQ1H0c= +github.com/lib/pq v0.0.0-20180201184707-88edab080323/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/magiconair/properties v1.7.6 h1:U+1DqNen04MdEPgFiIwdOUiqZ8qPa37xgogX/sd3+54= github.com/magiconair/properties v1.7.6/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/pkcs11 v0.0.0-20190329070431-55f3fac3af27/go.mod h1:WCBAbTOdfhHhz7YXujeZMF7owC4tPb1naKFsgfUISjo= +github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238 h1:+MZW2uvHgN8kYvksEN3f7eFL2wpzk0GxmlFsMybWc7E= github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.3.2 h1:mRS76wmkOn3KkKAyXDu42V+6ebnXWIztFSYGN7GeoRg= +github.com/mitchellh/mapstructure v1.3.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= +github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= github.com/pelletier/go-toml v1.1.0 h1:cmiOvKzEunMsAxyhXSzpL5Q1CRKpVv0KQsnAIcSEVYM= github.com/pelletier/go-toml v1.1.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.8.0 h1:Keo9qb7iRJs2voHvunFtuuYFsbWeOBh8/P9v/kVMFtw= +github.com/pelletier/go-toml v1.8.0/go.mod h1:D6yutnOGMveHEPV7VQOuvI/gXY61bv+9bAOTRnLElKs= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v0.8.0 h1:1921Yw9Gc3iSc4VQh3PIoOqgPCZS7G/4xQNVUp8Mda8= github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.1.0 h1:BQ53HtBmfOitExawJ6LokA4x8ov/z0SYYb0+HxJfRI8= +github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 h1:idejC8f05m9MGOsuEi1ATq9shN03HrxNkD/luQvxCv8= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.0.0-20180518154759-7600349dcfe1 h1:osmNoEW2SCW3L7EX0km2LYM8HKpNWRiouxjE3XHkyGc= github.com/prometheus/common v0.0.0-20180518154759-7600349dcfe1/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.6.0 h1:kRhiuYSXR3+uv2IbVbZhUxK5zVD/2pp3Gd2PpvPkpEo= +github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= github.com/prometheus/procfs v0.0.0-20180705121852-ae68e2d4c00f h1:c9M4CCa6g8WURSsbrl3lb/w/G1Z5xZpYvhhjdcVDOkE= github.com/prometheus/procfs v0.0.0-20180705121852-ae68e2d4c00f/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.3 h1:CTwfnzjQ+8dS6MhHHu4YswVAD99sL2wjPqP+VkURmKE= +github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/spf13/afero v1.1.0 h1:bopulORc2JeYaxfHLvJa5NzxviA9PoWhpiiJkru7Ji4= github.com/spf13/afero v1.1.0/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.3.1 h1:GPTpEAuNr98px18yNQ66JllNil98wfRZ/5Ukny8FeQA= +github.com/spf13/afero v1.3.1/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= github.com/spf13/cast v1.2.0 h1:HHl1DSRbEQN2i8tJmtS6ViPyHx35+p51amrdsiTCrkg= github.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg= +github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/jwalterweatherman v0.0.0-20180109140146-7c0cea34c8ec h1:2ZXvIUGghLpdTVHR1UfvfrzoVlZaE/yOWC5LueIHZig= github.com/spf13/jwalterweatherman v0.0.0-20180109140146-7c0cea34c8ec/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.1 h1:aCvUg6QPl3ibpQUxyLkrEkCHtPqYJL4x9AuhqVqFis4= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.0.2 h1:Ncr3ZIuJn322w2k1qmzXDnkLAdQMlJqBa9kfAH+irso= github.com/spf13/viper v1.0.2/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM= +github.com/spf13/viper v1.1.1 h1:/8JBRFO4eoHu1TmpsLgNBq1CQgRUg4GolYlEFieqJgo= +github.com/spf13/viper v1.1.1/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= +github.com/weppos/publicsuffix-go v0.4.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k= +github.com/weppos/publicsuffix-go v0.5.0 h1:rutRtjBJViU/YjcI5d80t4JAVvDltS6bciJg2K1HrLU= +github.com/weppos/publicsuffix-go v0.5.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k= +github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= +github.com/zmap/rc2 v0.0.0-20131011165748-24b9757f5521/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE= +github.com/zmap/zcertificate v0.0.0-20180516150559-0e3d58b1bac4/go.mod h1:5iU54tB79AMBcySS0R2XIyZBAVmeHranShAFELYx7is= +github.com/zmap/zcrypto v0.0.0-20190729165852-9051775e6a2e h1:mvOa4+/DXStR4ZXOks/UsjeFdn5O5JpLUtzqk9U8xXw= +github.com/zmap/zcrypto v0.0.0-20190729165852-9051775e6a2e/go.mod h1:w7kd3qXHh8FNaczNjslXqvFQiv5mMWRXlL9klTUAHc8= +github.com/zmap/zlint v0.0.0-20190806154020-fd021b4cfbeb h1:vxqkjztXSaPVDc8FQCdHTaejm2x747f6yPbnu1h2xkg= +github.com/zmap/zlint v0.0.0-20190806154020-fd021b4cfbeb/go.mod h1:29UiAJNsiVdvTBFCJW8e3q6dcDbOoPkhMgttOSCIMMY= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d h1:1ZiEyfaQIg3Qh0EoqpwAakHVhecoE5wlSg5GjnafJGw= +golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980 h1:dfGZHvZk057jK2MCeWus/TowKpJ8y4AmooUzdBSR9GU= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3 h1:4y9KwBHBgBNwDbtu44R5o1fdOCQUEXhbk/P4A9WmJq0= +golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190327125643-d831d65fe17d h1:XB2jc5XQ9uhizGTS2vWcN01bc4dI6z3C4KY5MQm8SS8= google.golang.org/genproto v0.0.0-20190327125643-d831d65fe17d/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0 h1:AzbTB6ux+okLTzP8Ru1Xs41C303zdcfEht7MQnYJt5A= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.29.1 h1:EC2SB8S04d2r73uptxphDSUG+kTKVgjRPF+N3xpxRB4= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/ci/scripts/run-test-network-basic.sh b/ci/scripts/run-test-network-basic.sh index 58cf8168..321df301 100755 --- a/ci/scripts/run-test-network-basic.sh +++ b/ci/scripts/run-test-network-basic.sh @@ -24,13 +24,13 @@ function stopNetwork() { } # Run Go application -#createNetwork -#print "Initializing Go application" -#pushd ../asset-transfer-basic/application-go -#print "Executing AssetTransfer.go" -#go run . -#popd -#stopNetwork +createNetwork +print "Initializing Go application" +pushd ../asset-transfer-basic/application-go +print "Executing AssetTransfer.go" +go run . +popd +stopNetwork # Run Java application createNetwork From d081d0cd3fbddf99bdac75804857676baf37a52c Mon Sep 17 00:00:00 2001 From: nikhil550 Date: Sun, 18 Oct 2020 13:50:39 -0400 Subject: [PATCH 19/38] update high througput sample to use deployCC script (#353) Signed-off-by: Nikhil Gupta --- high-throughput/README.md | 5 --- .../scripts/approve-commit-chaincode.sh | 44 ------------------- high-throughput/scripts/install-chaincode.sh | 28 ------------ high-throughput/startFabric.sh | 15 +------ 4 files changed, 2 insertions(+), 90 deletions(-) delete mode 100755 high-throughput/scripts/approve-commit-chaincode.sh delete mode 100755 high-throughput/scripts/install-chaincode.sh diff --git a/high-throughput/README.md b/high-throughput/README.md index 0badf673..d952a23e 100644 --- a/high-throughput/README.md +++ b/high-throughput/README.md @@ -96,11 +96,6 @@ must be verified before approval and admittance to the chain. ## How This sample provides the chaincode and scripts required to run a high-throughput application on the Fabric test network. -### Vendor the chaincode dependencies -1. Change into the chaincode directory, e.g. `cd ~/fabric-samples/high-throughput/chaincode` -2. Vendor the Go dependencies by running the following command: `GO111MODULE=on go mod vendor` -3. The chaincode directory will now contain a `vendor` directory. - ### Start the network You can use the `startFabric.sh` script to create an instance of the Fabric test network with a single channel named `mychannel`. The script then deploys the `high-throughput` chaincode to the channel by installing it on the test network peers and committing the chaincode definition to the channel. diff --git a/high-throughput/scripts/approve-commit-chaincode.sh b/high-throughput/scripts/approve-commit-chaincode.sh deleted file mode 100755 index 054ddb53..00000000 --- a/high-throughput/scripts/approve-commit-chaincode.sh +++ /dev/null @@ -1,44 +0,0 @@ -# -# Copyright IBM Corp All Rights Reserved -# -# SPDX-License-Identifier: Apache-2.0 -# - -export CORE_PEER_TLS_ENABLED=true - -echo "========== Query chaincode package ID ==========" -export CORE_PEER_MSPCONFIGPATH=../test-network/organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp -export CORE_PEER_ADDRESS=localhost:7051 -export CORE_PEER_LOCALMSPID="Org1MSP" -export CORE_PEER_TLS_ROOTCERT_FILE=../test-network/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt -peer lifecycle chaincode queryinstalled >&log.txt -export PACKAGE_ID=`sed -n '/Package/{s/^Package ID: //; s/, Label:.*$//; p;}' log.txt` - -echo "========== Approve definition for Org1 ==========" -export CORE_PEER_MSPCONFIGPATH=../test-network/organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp -export CORE_PEER_ADDRESS=localhost:7051 -export CORE_PEER_LOCALMSPID="Org1MSP" -export CORE_PEER_TLS_ROOTCERT_FILE=../test-network/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt -peer lifecycle chaincode install bigdatacc.tar.gz - -peer lifecycle chaincode approveformyorg -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ../test-network/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem --channelID mychannel --signature-policy "OR('Org1MSP.peer', 'Org2MSP.peer')" --name bigdatacc --version 0 --init-required --package-id ${PACKAGE_ID} --sequence 1 - -echo "========== Approve definition for Org2 ==========" -export CORE_PEER_MSPCONFIGPATH=../test-network/organizations/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp -export CORE_PEER_ADDRESS=localhost:9051 -export CORE_PEER_LOCALMSPID="Org2MSP" -export CORE_PEER_TLS_ROOTCERT_FILE=../test-network/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt -peer lifecycle chaincode approveformyorg -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ../test-network/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem --channelID mychannel --signature-policy "OR('Org1MSP.peer', 'Org2MSP.peer')" --name bigdatacc --version 0 --init-required --package-id ${PACKAGE_ID} --sequence 1 - -. scripts/check-commit-readiness.sh - -checkCommitReadiness 1 "\"Org1MSP\": true" "\"Org2MSP\": true" -checkCommitReadiness 2 "\"Org1MSP\": true" "\"Org2MSP\": true" - - -echo "========== Commit the definition the mychannel ==========" -peer lifecycle chaincode commit -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ../test-network/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem --channelID mychannel --signature-policy "OR('Org1MSP.peer', 'Org2MSP.peer')" --name bigdatacc --version 0 --init-required --sequence 1 --waitForEvent --peerAddresses localhost:7051 --tlsRootCertFiles ../test-network/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt --peerAddresses localhost:9051 --tlsRootCertFiles ../test-network/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt - - -echo "========== Invoke the Init function ==========" -peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ../test-network/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n bigdatacc --isInit -c '{"Args":["Init"]}' diff --git a/high-throughput/scripts/install-chaincode.sh b/high-throughput/scripts/install-chaincode.sh deleted file mode 100755 index cac7f4ac..00000000 --- a/high-throughput/scripts/install-chaincode.sh +++ /dev/null @@ -1,28 +0,0 @@ -# -# Copyright IBM Corp All Rights Reserved -# -# SPDX-License-Identifier: Apache-2.0 -# -export CORE_PEER_TLS_ENABLED=true - -echo "========== Package a chaincode ==========" -export CORE_PEER_MSPCONFIGPATH=../test-network/organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp -export CORE_PEER_ADDRESS=localhost:7051 -export CORE_PEER_LOCALMSPID="Org1MSP" -export CORE_PEER_TLS_ROOTCERT_FILE=../test-network/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt -peer lifecycle chaincode package bigdatacc.tar.gz --path chaincode/ --lang golang --label bigdatacc_0 - -echo "========== Installing chaincode on peer0.org1 ==========" -export CORE_PEER_MSPCONFIGPATH=../test-network/organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp -export CORE_PEER_ADDRESS=localhost:7051 -export CORE_PEER_LOCALMSPID="Org1MSP" -export CORE_PEER_TLS_ROOTCERT_FILE=../test-network/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt -peer lifecycle chaincode install bigdatacc.tar.gz - - -echo "========== Installing chaincode on peer0.org2 ==========" -export CORE_PEER_MSPCONFIGPATH=../test-network/organizations/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp -export CORE_PEER_ADDRESS=localhost:9051 -export CORE_PEER_LOCALMSPID="Org2MSP" -export CORE_PEER_TLS_ROOTCERT_FILE=../test-network/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt -peer lifecycle chaincode install bigdatacc.tar.gz diff --git a/high-throughput/startFabric.sh b/high-throughput/startFabric.sh index eb4dd8ef..20bec562 100755 --- a/high-throughput/startFabric.sh +++ b/high-throughput/startFabric.sh @@ -18,20 +18,9 @@ pushd ../test-network ./network.sh down echo "Bring up test network" -./network.sh up createChannel +./network.sh up createChannel -ca +./network.sh deployCC -ccn bigdatacc -ccp ../high-throughput/chaincode/ -ccep "OR('Org1MSP.peer','Org2MSP.peer')" -cci Init popd - -#set enviroment varialbes -export PATH=${PWD}/../bin:${PWD}:$PATH -export FABRIC_CFG_PATH=$PWD/../config/ - -echo "Install high throughput chaincode on test network peers" -./scripts/install-chaincode.sh - -echo "Deploy high throughput chaincode to the channel" -./scripts/approve-commit-chaincode.sh - - cat < Date: Mon, 19 Oct 2020 14:05:52 -0400 Subject: [PATCH 20/38] Include link to external chaincode builder doc (#356) Signed-off-by: Nikhil Gupta --- asset-transfer-basic/chaincode-external/README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/asset-transfer-basic/chaincode-external/README.md b/asset-transfer-basic/chaincode-external/README.md index e5b6bed9..1b4a294e 100755 --- a/asset-transfer-basic/chaincode-external/README.md +++ b/asset-transfer-basic/chaincode-external/README.md @@ -1,8 +1,6 @@ # Asset-Transfer-Basic as an external service -See the "Chaincode as an external service" documentation for an introduction on how to run chaincode as an external service. -This sample includes the external builder and launcher scripts which the peers in your Fabric network will require -in order to run an asset transfer chaincode as an external service. +This sample provides an introduction to how to use external builder and launcher scripts to run chaincode as an external service to your peer. For more information, see the [Chaincode as an external service](https://hyperledger-fabric.readthedocs.io/en/latest/cc_launcher.html) topic in the Fabric documentation. **Note:** each organization in a real network would need to setup and host their own instance of the external service. For simplification purpose, in this sample we use the same instance for both organizations. From 524ee2d63fa4e3dcef2b9bcbfe03ee8dd6c79e35 Mon Sep 17 00:00:00 2001 From: Bret Harrison Date: Tue, 20 Oct 2020 16:55:46 -0400 Subject: [PATCH 21/38] Have the submitTransaction look at the results of submit. The results of the submit must be returned by the chaincode. Signed-off-by: Bret Harrison --- asset-transfer-basic/application-javascript/app.js | 12 ++++++++++-- .../chaincode-javascript/lib/assetTransfer.js | 3 ++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/asset-transfer-basic/application-javascript/app.js b/asset-transfer-basic/application-javascript/app.js index 43fd6260..28311489 100644 --- a/asset-transfer-basic/application-javascript/app.js +++ b/asset-transfer-basic/application-javascript/app.js @@ -128,8 +128,16 @@ async function main() { // 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'); + result = await contract.submitTransaction('CreateAsset', 'asset13', 'yellow', '5', 'Tom', '1300'); + // The "submitTransaction" returns the value generated by the chaincode. Notice how we normally do not + // look at this value as the chaincodes are not returning a value. So for demonstration purposes we + // have the javascript version of the chaincode return a value on the function 'CreateAsset'. + // This value will be the same as the 'ReadAsset' results for the newly created asset. + // The other chaincode versions could be updated to also return a value. + // Having the chaincode return a value after after doing a create or update could avoid the application + // from making an "evaluateTransaction" call to get information on the asset added by the chaincode + // during the create or update. + console.log(`*** Result committed: ${prettyJSONString(result.toString())}`); console.log('\n--> Evaluate Transaction: ReadAsset, function returns an asset with a given assetID'); result = await contract.evaluateTransaction('ReadAsset', 'asset13'); diff --git a/asset-transfer-basic/chaincode-javascript/lib/assetTransfer.js b/asset-transfer-basic/chaincode-javascript/lib/assetTransfer.js index 6927eca1..d3a4fc47 100644 --- a/asset-transfer-basic/chaincode-javascript/lib/assetTransfer.js +++ b/asset-transfer-basic/chaincode-javascript/lib/assetTransfer.js @@ -72,7 +72,8 @@ class AssetTransfer extends Contract { Owner: owner, AppraisedValue: appraisedValue, }; - return ctx.stub.putState(id, Buffer.from(JSON.stringify(asset))); + ctx.stub.putState(id, Buffer.from(JSON.stringify(asset))); + return JSON.stringify(asset); } // ReadAsset returns the asset stored in the world state with given id. From 6d47d06a9b631766f4498efacdde6f24a3cd2d04 Mon Sep 17 00:00:00 2001 From: Julian Castrence Date: Mon, 19 Oct 2020 18:01:34 -0400 Subject: [PATCH 22/38] Implemented TotalSupply function of ERC-20 Token Standard FAB-18275 Signed-off-by: Julian Castrence --- .../chaincode-go/chaincode/token_contract.go | 47 ++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/token-account-based/chaincode-go/chaincode/token_contract.go b/token-account-based/chaincode-go/chaincode/token_contract.go index f9f34e8c..b7b2783b 100644 --- a/token-account-based/chaincode-go/chaincode/token_contract.go +++ b/token-account-based/chaincode-go/chaincode/token_contract.go @@ -8,6 +8,9 @@ import ( "github.com/hyperledger/fabric-contract-api-go/contractapi" ) +// Define key names for options +const totalSupplyKey = "totalSupply" + // SmartContract provides functions for transferring tokens between accounts type SmartContract struct { contractapi.Contract @@ -58,6 +61,25 @@ func (s *SmartContract) Mint(ctx contractapi.TransactionContextInterface, amount log.Printf("minter account %s balance updated from %d to %d", minter, currentBalance, updatedBalance) + // Update the totalSupply + totalSupplyBytes, err := ctx.GetStub().GetState(totalSupplyKey) + if err != nil { + return fmt.Errorf("failed to retrieve total token supply: %v", err) + } + + var totalSupply int + + // If no tokens have been minted, initialize the totalSupply + if totalSupplyBytes == nil { + totalSupply = 0 + } else { + totalSupply, _ = strconv.Atoi(string(totalSupplyBytes)) // Error handling not needed since Itoa() was used when setting the totalSupply, guaranteeing it was an integer. + } + + // Add the mint amount to the total supply and update the state + totalSupply += amount + ctx.GetStub().PutState(totalSupplyKey, []byte(strconv.Itoa(totalSupply))) + return nil } @@ -92,7 +114,7 @@ func (s *SmartContract) Transfer(ctx contractapi.TransactionContextInterface, re recipientCurrentBalanceBytes, err := ctx.GetStub().GetState(recipient) if err != nil { - fmt.Errorf("failed to read recipient account %s from world state: %v", recipient, err) + return fmt.Errorf("failed to read recipient account %s from world state: %v", recipient, err) } var recipientCurrentBalance int @@ -172,3 +194,26 @@ func (s *SmartContract) ClientAccountID(ctx contractapi.TransactionContextInterf return clientAccountID, nil } + +// TotalSupply returns the total token supply +func (s *SmartContract) TotalSupply(ctx contractapi.TransactionContextInterface) (int, error) { + + // Retrieve total supply of tokens from state of smart contract + totalSupplyBytes, err := ctx.GetStub().GetState(totalSupplyKey) + if err != nil { + return 0, fmt.Errorf("failed to retrieve total token supply: %v", err) + } + + var totalSupply int + + // If no tokens have been minted, return 0 + if totalSupplyBytes == nil { + totalSupply = 0 + } else { + totalSupply, _ = strconv.Atoi(string(totalSupplyBytes)) // Error handling not needed since Itoa() was used when setting the totalSupply, guaranteeing it was an integer. + } + + log.Printf("The totalSupply was queried: %d total tokens", totalSupply) + return totalSupply, nil + +} From f9bd0097b945d9fe9d8b904002bc904cf4d6f8d1 Mon Sep 17 00:00:00 2001 From: Jonas Kreusch Date: Wed, 21 Oct 2020 11:43:14 +0200 Subject: [PATCH 23/38] Update buy.js Fixed a typo. It should be "buy" instead of "issue" Signed-off-by: Jonas Kreusch --- commercial-paper/organization/digibank/application/buy.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commercial-paper/organization/digibank/application/buy.js b/commercial-paper/organization/digibank/application/buy.js index f71508b6..b052b75d 100644 --- a/commercial-paper/organization/digibank/application/buy.js +++ b/commercial-paper/organization/digibank/application/buy.js @@ -9,7 +9,7 @@ * 1. Select an identity from a wallet * 2. Connect to network gateway * 3. Access PaperNet network - * 4. Construct request to issue commercial paper + * 4. Construct request to buy commercial paper * 5. Submit transaction * 6. Process response */ From 1794c026c994ebf27afe943e6e3174b99872bb02 Mon Sep 17 00:00:00 2001 From: David Enyeart Date: Tue, 20 Oct 2020 15:18:59 -0400 Subject: [PATCH 24/38] Update CouchDB to 3.1.1 Update CouchDB in CI and test-network to 3.1.1. Signed-off-by: David Enyeart --- ci/scripts/pullFabricImages.sh | 2 +- test-network/addOrg3/docker/docker-compose-couch-org3.yaml | 2 +- test-network/docker/docker-compose-couch.yaml | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ci/scripts/pullFabricImages.sh b/ci/scripts/pullFabricImages.sh index f58566b3..38cb8e2d 100755 --- a/ci/scripts/pullFabricImages.sh +++ b/ci/scripts/pullFabricImages.sh @@ -11,5 +11,5 @@ for image in baseos peer orderer ca tools orderer ccenv javaenv nodeenv tools; d docker rmi -f "hyperledger-fabric.jfrog.io/fabric-${image}:${STABLE_TAG}" done -docker pull -q couchdb:3.1 +docker pull -q couchdb:3.1.1 docker images | grep hyperledger diff --git a/test-network/addOrg3/docker/docker-compose-couch-org3.yaml b/test-network/addOrg3/docker/docker-compose-couch-org3.yaml index cad09f28..8c08a24e 100644 --- a/test-network/addOrg3/docker/docker-compose-couch-org3.yaml +++ b/test-network/addOrg3/docker/docker-compose-couch-org3.yaml @@ -11,7 +11,7 @@ networks: services: couchdb4: container_name: couchdb4 - image: couchdb:3.1 + image: couchdb:3.1.1 # Populate the COUCHDB_USER and COUCHDB_PASSWORD to set an admin user and password # for CouchDB. This will prevent CouchDB from operating in an "Admin Party" mode. environment: diff --git a/test-network/docker/docker-compose-couch.yaml b/test-network/docker/docker-compose-couch.yaml index c0906fd0..1b84e129 100644 --- a/test-network/docker/docker-compose-couch.yaml +++ b/test-network/docker/docker-compose-couch.yaml @@ -11,7 +11,7 @@ networks: services: couchdb0: container_name: couchdb0 - image: couchdb:3.1 + image: couchdb:3.1.1 # Populate the COUCHDB_USER and COUCHDB_PASSWORD to set an admin user and password # for CouchDB. This will prevent CouchDB from operating in an "Admin Party" mode. environment: @@ -38,7 +38,7 @@ services: couchdb1: container_name: couchdb1 - image: couchdb:3.1 + image: couchdb:3.1.1 # Populate the COUCHDB_USER and COUCHDB_PASSWORD to set an admin user and password # for CouchDB. This will prevent CouchDB from operating in an "Admin Party" mode. environment: From 3fbce62c01ac168cabd7ed6b07a6ca0f19003f15 Mon Sep 17 00:00:00 2001 From: Arnaud J Le Hors Date: Tue, 27 Oct 2020 17:07:49 +0100 Subject: [PATCH 25/38] Fix commercial paper org scripts (#362) The current scripts given to setup the user environment do not work on Mac: $ . ./magnetocorp.sh -bash: cd: /Users/lehors/Projects/Go/src/github.com/hyperledger/fabric-samples/commercial-paper/organization/magnetocorp Saving session... ...copying shared history... ...saving history...truncating history files... ...completed./../../../test-network: No such file or directory -bash: ./scripts/envVar.sh: No such file or directory [...truncated...] export FABRIC_CFG_PATH="/Users/lehors/Projects/Go/src/github.com/hyperledger/fabric-samples/commercial-paper/organization/magnetocorp" export PATH="/Users/lehors/Projects/Go/src/github.com/hyperledger/fabric-samples/commercial-paper/organization/magnetocorp" export PEER_PARMS="" Saving session... Saving session... The session history related output is actually captured when setting the environment variables: $ echo $FABRIC_CFG_PATH /Users/lehors/Projects/Go/src/github.com/hyperledger/fabric-samples/commercial-paper/organization/magnetocorp Saving session... ...copying shared history... ...saving history...truncating history files... ...completed./../../../config With this simple change the scripts work: $ echo $FABRIC_CFG_PATH /Users/lehors/Projects/Go/src/github.com/hyperledger/fabric-samples/commercial-paper/organization/magnetocorp/../../../config Signed-off-by: Arnaud J Le Hors --- commercial-paper/organization/digibank/digibank.sh | 4 ++-- commercial-paper/organization/magnetocorp/magnetocorp.sh | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/commercial-paper/organization/digibank/digibank.sh b/commercial-paper/organization/digibank/digibank.sh index 3aad75d2..414d4d2e 100755 --- a/commercial-paper/organization/digibank/digibank.sh +++ b/commercial-paper/organization/digibank/digibank.sh @@ -15,7 +15,7 @@ function _exit(){ : ${VERBOSE:="false"} # Where am I? -DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +DIR=${PWD} # Locate the test network cd "${DIR}/../../../test-network" @@ -36,4 +36,4 @@ env | sort | comm -1 -3 /tmp/env.orig - | sed -E 's/(.*)=(.*)/export \1="\2"/' rm /tmp/env.orig -cd ${DIR} \ No newline at end of file +cd ${DIR} diff --git a/commercial-paper/organization/magnetocorp/magnetocorp.sh b/commercial-paper/organization/magnetocorp/magnetocorp.sh index af456384..784602ca 100755 --- a/commercial-paper/organization/magnetocorp/magnetocorp.sh +++ b/commercial-paper/organization/magnetocorp/magnetocorp.sh @@ -15,7 +15,7 @@ function _exit(){ : ${VERBOSE:="false"} # Where am I? -DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +DIR=${PWD} # Locate the test-network cd "${DIR}/../../../test-network" @@ -35,4 +35,4 @@ export PATH="${DIR}/../../../bin:${PWD}:$PATH" env | sort | comm -1 -3 /tmp/env.orig - | sed -E 's/(.*)=(.*)/export \1="\2"/' rm /tmp/env.orig -cd ${DIR} \ No newline at end of file +cd ${DIR} From e8d6e468af025810b5056ee2fe611511a925ed29 Mon Sep 17 00:00:00 2001 From: Yuki Kondo <57741286+yukiknd@users.noreply.github.com> Date: Wed, 28 Oct 2020 23:57:44 +0900 Subject: [PATCH 26/38] [FAB-18228] Add ERC20 fungible token sample for chaincode javascript (#327) This PR adds ERC20 capabilities to the token-account-based sample. It includes javascript Chaincode and the updated README to explain how to use tokens in the Fabric test-network. Signed-off-by: Yuki Kondo --- token-account-based/README.md | 150 +++++++ .../chaincode-javascript/.editorconfig | 16 + .../chaincode-javascript/.eslintignore | 5 + .../chaincode-javascript/.eslintrc.js | 39 ++ .../chaincode-javascript/.gitignore | 78 ++++ .../chaincode-javascript/index.js | 10 + .../chaincode-javascript/lib/tokenERC20.js | 409 ++++++++++++++++++ .../chaincode-javascript/package.json | 51 +++ .../test/tokenERC20.test.js | 271 ++++++++++++ 9 files changed, 1029 insertions(+) create mode 100755 token-account-based/chaincode-javascript/.editorconfig create mode 100644 token-account-based/chaincode-javascript/.eslintignore create mode 100644 token-account-based/chaincode-javascript/.eslintrc.js create mode 100644 token-account-based/chaincode-javascript/.gitignore create mode 100644 token-account-based/chaincode-javascript/index.js create mode 100644 token-account-based/chaincode-javascript/lib/tokenERC20.js create mode 100644 token-account-based/chaincode-javascript/package.json create mode 100644 token-account-based/chaincode-javascript/test/tokenERC20.test.js diff --git a/token-account-based/README.md b/token-account-based/README.md index bceb976a..ca260fba 100644 --- a/token-account-based/README.md +++ b/token-account-based/README.md @@ -30,10 +30,17 @@ The -ca flag is used to deploy the network using certificate authorities. This a ## Deploy the smart contract to the channel You can use the test network script to deploy the account-based token contract to the channel that was just created. Deploy the smart contract to `mychannel` using the following command: + +**For a Go Contract:** ``` ./network.sh deployCC -ccn token_account -ccp ../token-account-based/chaincode-go/ ``` +**For a JavaScript Contract:** +``` +./network.sh deployCC -ccn token_account -ccp ../token-account-based/chaincode-javascript/ -ccl javascript +``` + The above command deploys the go chaincode with short name `token_account`. The smart contract will use the default endorsement policy of majority of channel members. Since the channel has two members, this implies that we'll need to get peer endorsements from 2 out of the 2 channel members. @@ -140,6 +147,8 @@ Using the Org2 terminal, the Org2 recipient user can retrieve their own account peer chaincode query -C mychannel -n token_account -c '{"function":"ClientAccountID","Args":[]}' ``` +**For a Go Contract:** + The function returns of recipient's account ID: ``` eDUwOTo6Q049cmVjaXBpZW50LE9VPWNsaWVudCxPPUh5cGVybGVkZ2VyLFNUPU5vcnRoIENhcm9saW5hLEM9VVM6OkNOPWNhLm9yZzIuZXhhbXBsZS5jb20sTz1vcmcyLmV4YW1wbGUuY29tLEw9SHVyc2xleSxTVD1IYW1wc2hpcmUsQz1VSw== @@ -155,12 +164,28 @@ The result shows that the subject and issuer is indeed the recipient user from O x509::CN=recipient,OU=client,O=Hyperledger,ST=North Carolina,C=US::CN=ca.org2.example.com,O=org2.example.com,L=Hursley,ST=Hampshire,C=UK ``` +**For a JavaScript Contract:** + +The function returns of recipient's client ID. +The result shows that the subject and issuer is indeed the recipient user from Org2: +``` +x509::/C=US/ST=North Carolina/O=Hyperledger/OU=client/CN=recipient::/C=UK/ST=Hampshire/L=Hursley/O=org2.example.com/CN=ca.org2.example.com +``` + After the Org2 recipient provides their account ID to the minter, the minter can initiate a transfer from their account to the recipient's account. Back in the Org1 terminal, request the transfer of 100 tokens to the recipient account: + +**For a Go Contract:** ``` peer chaincode invoke $TARGET_TLS_OPTIONS -C mychannel -n token_account -c '{"function":"Transfer","Args":[ "eDUwOTo6Q049cmVjaXBpZW50LE9VPWNsaWVudCxPPUh5cGVybGVkZ2VyLFNUPU5vcnRoIENhcm9saW5hLEM9VVM6OkNOPWNhLm9yZzIuZXhhbXBsZS5jb20sTz1vcmcyLmV4YW1wbGUuY29tLEw9SHVyc2xleSxTVD1IYW1wc2hpcmUsQz1VSw==","100"]}' ``` +**For a JavaScript Contract:** +``` +export RECIPIENT="x509::/C=US/ST=North Carolina/O=Hyperledger/OU=client/CN=recipient::/C=UK/ST=Hampshire/L=Hursley/O=org2.example.com/CN=ca.org2.example.com" +peer chaincode invoke $TARGET_TLS_OPTIONS -C mychannel -n token_account -c '{"function":"Transfer","Args":[ "'"$RECIPIENT"'","100"]}' +``` + The `Transfer` function validates that the account associated with the calling client ID has sufficient funds for the transfer. It will then debit the caller's account and credit the recipient's account. Note that the sample contract will automatically create an account with zero balance for the recipient, if one does not yet exist. @@ -186,6 +211,131 @@ The function queries the balance of the account associated with the recipient cl Congratulations, you've transferred 100 tokens! The Org2 recipient can now transfer tokens to other registered users in the same manner. +## Another scenario (for JavaScript contract only) + +This sample has another transfer method called `transferFrom`, which allows an approved spender to transfer fungible tokens on behalf of the account owner. The second scenario demonstrates how to approve the spender and transfer fungible tokens. + +In this tutorial, you will approve the spender and transfer tokens as follows: + +- A minter has already created tokens according to the scenario above. +- The same minter client uses the `approve` function to set the allowance of tokens a spender client can transfer on behalf of the minter. It is assumed that the spender has provided their client ID to the `approve` caller out of band. +- The spender client will then use the `transferFrom` function to transfer the requested number of tokens to the recipient's account on behalf of the minter. It is assumed that the recipient has provided their client ID to the `transferFrom` caller out of band. + +## Register identities + +You have already brought up the network and deployed the smart contract to the channel. We will use the same network and smart contract. + +We will use the Org1 CA to create the spender identity. +Back in the Org1 terminal, you can register a new spender client identity using the `fabric-ca-client` tool: +``` +fabric-ca-client register --caname ca-org1 --id.name spender --id.secret spenderpw --id.type client --tls.certfiles ${PWD}/organizations/fabric-ca/org1/tls-cert.pem +``` + +You can now generate the identity certificates and MSP folder by providing the enroll name and secret to the enroll command: +``` +fabric-ca-client enroll -u https://spender:spenderpw@localhost:7054 --caname ca-org1 -M ${PWD}/organizations/peerOrganizations/org1.example.com/users/spender@org1.example.com/msp --tls.certfiles ${PWD}/organizations/fabric-ca/org1/tls-cert.pem +``` + +Run the command below to copy the Node OU configuration file into the spender identity MSP folder. +``` +cp ${PWD}/organizations/peerOrganizations/org1.example.com/msp/config.yaml ${PWD}/organizations/peerOrganizations/org1.example.com/users/spender@org1.example.com/msp/config.yaml +``` + +## Approve a spender + +The minter intends to approve 500 tokens to be transferred by the spender, but first the spender needs to provide their own client ID as the payment address. + +Open a 3rd terminal to represent the spender in Org1 and navigate to fabric-samples/test-network. Set the the environment variables for the Org1 spender user. + +``` +export PATH=${PWD}/../bin:${PWD}:$PATH +export FABRIC_CFG_PATH=$PWD/../config/ +export CORE_PEER_TLS_ENABLED=true +export CORE_PEER_LOCALMSPID="Org1MSP" +export CORE_PEER_MSPCONFIGPATH=${PWD}/organizations/peerOrganizations/org1.example.com/users/spender@org1.example.com/msp +export CORE_PEER_TLS_ROOTCERT_FILE=${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt +export CORE_PEER_ADDRESS=localhost:7051 +export TARGET_TLS_OPTIONS="-o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem --peerAddresses localhost:7051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt --peerAddresses localhost:9051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt" +``` + +Now the Org1 spender can retrieve their own client ID: +``` +peer chaincode query -C mychannel -n token_account -c '{"function":"ClientAccountID","Args":[]}' +``` + +The function returns of spender's client ID. +The result shows that the subject and issuer is indeed the recipient user from Org2: +``` +x509::/C=US/ST=North Carolina/O=Hyperledger/OU=client/CN=spender::/C=US/ST=North Carolina/L=Durham/O=org1.example.com/CN=ca.org1.example.com +``` + +After the Org1 spender provides their client ID to the minter, the minter can approve a spender. +Back in the Org1 terminal, request the approval of 100 tokens to be withdrew by the spender.: +``` +export SPENDER="x509::/C=US/ST=North Carolina/O=Hyperledger/OU=client/CN=spender::/C=US/ST=North Carolina/L=Durham/O=org1.example.com/CN=ca.org1.example.com" +peer chaincode invoke $TARGET_TLS_OPTIONS -C mychannel -n token_account -c '{"function":"Approve","Args":["'"$SPENDER"'", "500"]}' +``` + +The approve function added that the spender client can consume 500 tokens on behalf of the minter. We can check the spender client's allowance from the minter by calling the `allowance` function. + +Let's request the spender's allowance from the minter: +``` +export MINTER="x509::/C=US/ST=North Carolina/O=Hyperledger/OU=client/CN=minter::/C=US/ST=North Carolina/L=Durham/O=org1.example.com/CN=ca.org1.example.com" +peer chaincode query -C mychannel -n token_account -c '{"function":"Allowance","Args":["'"$MINTER"'", "'"$SPENDER"'"]}' +``` + +The function queries the allowance associated with the spender client ID and returns: +``` +500 +``` + +## TransferFrom tokens + +The spender intends to transfer 100 tokens to the Org2 recipient on behalf of the minter. The spender has already got the minter client Id and the recipient client ID. + +Back in the 3rd terminal, request the transfer of 100 tokens to the recipient account: +``` +export MINTER="x509::/C=US/ST=North Carolina/O=Hyperledger/OU=client/CN=minter::/C=US/ST=North Carolina/L=Durham/O=org1.example.com/CN=ca.org1.example.com" +export RECIPIENT="x509::/C=US/ST=North Carolina/O=Hyperledger/OU=client/CN=recipient::/C=UK/ST=Hampshire/L=Hursley/O=org2.example.com/CN=ca.org2.example.com" +peer chaincode invoke $TARGET_TLS_OPTIONS -C mychannel -n token_account -c '{"function":"TransferFrom","Args":[ "'"$MINTER"'", "'"$RECIPIENT"'", "100"]}' +``` + +The `TransferFrom` function has three args: sender, recipient, amount. The function validates that the account associated with the sender has sufficient funds for the transfer. The function also validates if the allowance associated with the calling client ID exceeds funds to be transferred. +It will then debit the sender's account and credit the recipient's account. It will also decrease the spender's allowance approved by the minter. Note that the sample contract will automatically create an account with zero balance for the recipient, if one does not yet exist. + +While still in the 3rd terminal, let's request the minter's account balance again: +``` +peer chaincode query -C mychannel -n token_account -c '{"function":"BalanceOf","Args":["'"$MINTER"'"]}' +``` + +The function queries the balance of the account associated with the minter client ID and returns: +``` +4800 +``` + +While still in the 3rd terminal, let's request the spender's allowance from the minter again: +``` +export SPENDER="x509::/C=US/ST=North Carolina/O=Hyperledger/OU=client/CN=spender::/C=US/ST=North Carolina/L=Durham/O=org1.example.com/CN=ca.org1.example.com" +peer chaincode query -C mychannel -n token_account -c '{"function":"Allowance","Args":["'"$MINTER"'", "'"$SPENDER"'"]}' +``` + +The function queries the allowance associated with the spender client ID and returns: +``` +400 +``` + +And then using the Org2 terminal, let's request the recipient's balance: +``` +peer chaincode query -C mychannel -n token_account -c '{"function":"ClientAccountBalance","Args":[]}' +``` + +The function queries the balance of the account associated with the recipient client ID and returns: +``` +200 +``` + +Congratulations, you've transferred 100 tokens! The Org2 recipient can now transfer tokens to other registered users in the same manner. + ## Clean up When you are finished, you can bring down the test network. The command will remove all the nodes of the test network, and delete any ledger data that you created: diff --git a/token-account-based/chaincode-javascript/.editorconfig b/token-account-based/chaincode-javascript/.editorconfig new file mode 100755 index 00000000..75a13be2 --- /dev/null +++ b/token-account-based/chaincode-javascript/.editorconfig @@ -0,0 +1,16 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# + +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false diff --git a/token-account-based/chaincode-javascript/.eslintignore b/token-account-based/chaincode-javascript/.eslintignore new file mode 100644 index 00000000..15958470 --- /dev/null +++ b/token-account-based/chaincode-javascript/.eslintignore @@ -0,0 +1,5 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# + +coverage diff --git a/token-account-based/chaincode-javascript/.eslintrc.js b/token-account-based/chaincode-javascript/.eslintrc.js new file mode 100644 index 00000000..8d99762d --- /dev/null +++ b/token-account-based/chaincode-javascript/.eslintrc.js @@ -0,0 +1,39 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +module.exports = { + env: { + node: true, + es6: true, + mocha: true + }, + parserOptions: { + ecmaVersion: 8, + sourceType: 'script' + }, + extends: "eslint:recommended", + rules: { + indent: ['error', 4], + 'linebreak-style': ['error', 'unix'], + quotes: ['error', 'single'], + semi: ['error', 'always'], + 'no-unused-vars': ['error', { args: 'none' }], + 'no-console': 'off', + curly: 'error', + eqeqeq: 'error', + 'no-throw-literal': 'error', + strict: 'error', + 'no-var': 'error', + 'dot-notation': 'error', + 'no-tabs': 'error', + 'no-trailing-spaces': 'error', + 'no-use-before-define': 'error', + 'no-useless-call': 'error', + 'no-with': 'error', + 'operator-linebreak': 'error', + yoda: 'error', + 'quote-props': ['error', 'as-needed'], + 'no-constant-condition': ["error", { "checkLoops": false }] + } +}; diff --git a/token-account-based/chaincode-javascript/.gitignore b/token-account-based/chaincode-javascript/.gitignore new file mode 100644 index 00000000..c84ff1db --- /dev/null +++ b/token-account-based/chaincode-javascript/.gitignore @@ -0,0 +1,78 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ +package-lock.json + +# TypeScript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# next.js build output +.next + +# nuxt.js build output +.nuxt + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless diff --git a/token-account-based/chaincode-javascript/index.js b/token-account-based/chaincode-javascript/index.js new file mode 100644 index 00000000..1841d315 --- /dev/null +++ b/token-account-based/chaincode-javascript/index.js @@ -0,0 +1,10 @@ +/* +SPDX-License-Identifier: Apache-2.0 +*/ + +'use strict'; + +const tokenERC20Contract = require('./lib/tokenERC20.js'); + +module.exports.TokenERC20Contract = tokenERC20Contract; +module.exports.contracts = [tokenERC20Contract]; \ No newline at end of file diff --git a/token-account-based/chaincode-javascript/lib/tokenERC20.js b/token-account-based/chaincode-javascript/lib/tokenERC20.js new file mode 100644 index 00000000..7ca67749 --- /dev/null +++ b/token-account-based/chaincode-javascript/lib/tokenERC20.js @@ -0,0 +1,409 @@ +/* +SPDX-License-Identifier: Apache-2.0 +*/ + +'use strict'; + +const { Contract } = require('fabric-contract-api'); + +// Define objectType names for prefix +const balancePrefix = 'balance'; +const allowancePrefix = 'allowance'; + +// Define key names for options +const nameKey = 'name'; +const symbolKey = 'symbol'; +const decimalsKey = 'decimals'; +const totalSupplyKey = 'totalSupply'; + +class TokenERC20Contract extends Contract { + + /** + * Return the name of the token - e.g. "MyToken". + * The original function name is `name` in ERC20 specification. + * However, 'name' conflicts with a parameter `name` in `Contract` class. + * As a work around, we use `TokenName` as an alternative function name. + * + * @param {Context} ctx the transaction context + * @returns {String} Returns the name of the token + */ + async TokenName(ctx) { + const nameBytes = await ctx.stub.getState(nameKey); + return nameBytes.toString(); + } + + /** + * Return the symbol of the token. E.g. “HIX”. + * + * @param {Context} ctx the transaction context + * @returns {String} Returns the symbol of the token + */ + async Symbol(ctx) { + const symbolBytes = await ctx.stub.getState(symbolKey); + return symbolBytes.toString(); + } + + /** + * Return the number of decimals the token uses + * e.g. 8, means to divide the token amount by 100000000 to get its user representation. + * + * @param {Context} ctx the transaction context + * @returns {Number} Returns the number of decimals + */ + async Decimals(ctx) { + const decimalsBytes = await ctx.stub.getState(decimalsKey); + const decimals = parseInt(decimalsBytes.toString()); + return decimals; + } + + /** + * Return the total token supply. + * + * @param {Context} ctx the transaction context + * @returns {Number} Returns the total token supply + */ + async TotalSupply(ctx) { + const totalSupplyBytes = await ctx.stub.getState(totalSupplyKey); + const totalSupply = parseInt(totalSupplyBytes.toString()); + return totalSupply; + } + + /** + * BalanceOf returns the balance of the given account. + * + * @param {Context} ctx the transaction context + * @param {String} owner The owner from which the balance will be retrieved + * @returns {Number} Returns the account balance + */ + async BalanceOf(ctx, owner) { + const balanceKey = ctx.stub.createCompositeKey(balancePrefix, [owner]); + + const balanceBytes = await ctx.stub.getState(balanceKey); + if (!balanceBytes || balanceBytes.length === 0) { + throw new Error(`the account ${owner} does not exist`); + } + const balance = parseInt(balanceBytes.toString()); + + return balance; + } + + /** + * Transfer transfers tokens from client account to recipient account. + * recipient account must be a valid clientID as returned by the ClientAccountID() function. + * + * @param {Context} ctx the transaction context + * @param {String} to The recipient + * @param {Integer} value The amount of token to be transferred + * @returns {Boolean} Return whether the transfer was successful or not + */ + async Transfer(ctx, to, value) { + const from = ctx.clientIdentity.getID(); + + const transferResp = await this._transfer(ctx, from, to, value); + if (!transferResp) { + throw new Error('Failed to transfer'); + } + + // Emit the Transfer event + const transferEvent = { from, to, value: parseInt(value) }; + ctx.stub.setEvent('Transfer', Buffer.from(JSON.stringify(transferEvent))); + + return true; + } + + /** + * Transfer `value` amount of tokens from `from` to `to`. + * + * @param {Context} ctx the transaction context + * @param {String} from The sender + * @param {String} to The recipient + * @param {Integer} value The amount of token to be transferred + * @returns {Boolean} Return whether the transfer was successful or not + */ + async TransferFrom(ctx, from, to, value) { + const spender = ctx.clientIdentity.getID(); + + // Retrieve the allowance of the spender + const allowanceKey = ctx.stub.createCompositeKey(allowancePrefix, [from, spender]); + const currentAllowanceBytes = await ctx.stub.getState(allowanceKey); + + if (!currentAllowanceBytes || currentAllowanceBytes.length === 0) { + throw new Error(`spender ${spender} has no allowance from ${from}`); + } + + const currentAllowance = parseInt(currentAllowanceBytes.toString()); + + // Convert value from string to int + const valueInt = parseInt(value); + + // Check if the transferred value is less than the allowance + if (currentAllowance < valueInt) { + throw new Error('The spender does not have enough allowance to spend.'); + } + + const transferResp = await this._transfer(ctx, from, to, value); + if (!transferResp) { + throw new Error('Failed to transfer'); + } + + // Decrease the allowance + const updatedAllowance = currentAllowance - valueInt; + await ctx.stub.putState(allowanceKey, Buffer.from(updatedAllowance.toString())); + console.log(`spender ${spender} allowance updated from ${currentAllowance} to ${updatedAllowance}`); + + // Emit the Transfer event + const transferEvent = { from, to, value: valueInt }; + ctx.stub.setEvent('Transfer', Buffer.from(JSON.stringify(transferEvent))); + + console.log('transferFrom ended successfully'); + return true; + } + + async _transfer(ctx, from, to, value) { + + // Convert value from string to int + const valueInt = parseInt(value); + + if (valueInt < 0) { // transfer of 0 is allowed in ERC20, so just validate against negative amounts + throw new Error('transfer amount cannot be negative'); + } + + // Retrieve the current balance of the sender + const fromBalanceKey = ctx.stub.createCompositeKey(balancePrefix, [from]); + const fromCurrentBalanceBytes = await ctx.stub.getState(fromBalanceKey); + + if (!fromCurrentBalanceBytes || fromCurrentBalanceBytes.length === 0) { + throw new Error(`client account ${from} has no balance`); + } + + const fromCurrentBalance = parseInt(fromCurrentBalanceBytes.toString()); + + // Check if the sender has enough tokens to spend. + if (fromCurrentBalance < valueInt) { + throw new Error(`client account ${from} has insufficient funds.`); + } + + // Retrieve the current balance of the recepient + const toBalanceKey = ctx.stub.createCompositeKey(balancePrefix, [to]); + const toCurrentBalanceBytes = await ctx.stub.getState(toBalanceKey); + + let toCurrentBalance; + // If recipient current balance doesn't yet exist, we'll create it with a current balance of 0 + if (!toCurrentBalanceBytes || toCurrentBalanceBytes.length === 0) { + toCurrentBalance = 0; + } else { + toCurrentBalance = parseInt(toCurrentBalanceBytes.toString()); + } + + // Update the balance + const fromUpdatedBalance = fromCurrentBalance - valueInt; + const toUpdatedBalance = toCurrentBalance + valueInt; + + await ctx.stub.putState(fromBalanceKey, Buffer.from(fromUpdatedBalance.toString())); + await ctx.stub.putState(toBalanceKey, Buffer.from(toUpdatedBalance.toString())); + + console.log(`client ${from} balance updated from ${fromCurrentBalance} to ${fromUpdatedBalance}`); + console.log(`recipient ${to} balance updated from ${toCurrentBalance} to ${toUpdatedBalance}`); + + return true; + } + + /** + * Allows `spender` to spend `value` amount of tokens from the owner. + * + * @param {Context} ctx the transaction context + * @param {String} spender The spender + * @param {Integer} value The amount of tokens to be approved for transfer + * @returns {Boolean} Return whether the approval was successful or not + */ + async Approve(ctx, spender, value) { + const owner = ctx.clientIdentity.getID(); + + const allowanceKey = ctx.stub.createCompositeKey(allowancePrefix, [owner, spender]); + + let valueInt = parseInt(value); + await ctx.stub.putState(allowanceKey, Buffer.from(valueInt.toString())); + + // Emit the Approval event + const approvalEvent = { owner, spender, value: valueInt }; + ctx.stub.setEvent('Approval', Buffer.from(JSON.stringify(approvalEvent))); + + console.log('approve ended successfully'); + return true; + } + + /** + * Returns the amount of tokens which `spender` is allowed to withdraw from `owner`. + * + * @param {Context} ctx the transaction context + * @param {String} owner The owner of tokens + * @param {String} spender The spender who are able to transfer the tokens + * @returns {Number} Return the amount of remaining tokens allowed to spent + */ + async Allowance(ctx, owner, spender) { + const allowanceKey = ctx.stub.createCompositeKey(allowancePrefix, [owner, spender]); + + const allowanceBytes = await ctx.stub.getState(allowanceKey); + if (!allowanceBytes || allowanceBytes.length === 0) { + throw new Error(`spender ${spender} has no allowance from ${owner}`); + } + + const allowance = parseInt(allowanceBytes.toString()); + return allowance; + } + + // ================== Extended Functions ========================== + + /** + * Set optional infomation for a token. + * + * @param {Context} ctx the transaction context + * @param {String} name The name of the token + * @param {String} symbol The symbol of the token + * @param {String} decimals The decimals of the token + * @param {String} totalSupply The totalSupply of the token + */ + async SetOption(ctx, name, symbol, decimals) { + await ctx.stub.putState(nameKey, Buffer.from(name)); + await ctx.stub.putState(symbolKey, Buffer.from(symbol)); + await ctx.stub.putState(decimalsKey, Buffer.from(decimals)); + + console.log(`name: ${name}, symbol: ${symbol}, decimals: ${decimals}`); + return true; + } + + /** + * Mint creates new tokens and adds them to minter's account balance + * + * @param {Context} ctx the transaction context + * @param {Integer} amount amount of tokens to be minted + * @returns {Object} The balance + */ + async Mint(ctx, amount) { + + // Check minter authorization - this sample assumes Org1 is the central banker with privilege to mint new tokens + const clientMSPID = ctx.clientIdentity.getMSPID(); + if (clientMSPID !== 'Org1MSP') { + throw new Error('client is not authorized to mint new tokens'); + } + + // Get ID of submitting client identity + const minter = ctx.clientIdentity.getID(); + + const amountInt = parseInt(amount); + if (amountInt <= 0) { + throw new Error('mint amount must be a positive integer'); + } + + const balanceKey = ctx.stub.createCompositeKey(balancePrefix, [minter]); + + const currentBalanceBytes = await ctx.stub.getState(balanceKey); + // If minter current balance doesn't yet exist, we'll create it with a current balance of 0 + let currentBalance; + if (!currentBalanceBytes || currentBalanceBytes.length === 0) { + currentBalance = 0; + } else { + currentBalance = parseInt(currentBalanceBytes.toString()); + } + const updatedBalance = currentBalance + amountInt; + + await ctx.stub.putState(balanceKey, Buffer.from(updatedBalance.toString())); + + // Increase totalSupply + const totalSupplyBytes = await ctx.stub.getState(totalSupplyKey); + let totalSupply; + if (!totalSupplyBytes || totalSupplyBytes.length === 0) { + console.log('Initialize the tokenSupply'); + totalSupply = 0; + } else { + totalSupply = parseInt(totalSupplyBytes.toString()); + } + totalSupply = totalSupply + amountInt; + await ctx.stub.putState(totalSupplyKey, Buffer.from(totalSupply.toString())); + + // Emit the Transfer event + const transferEvent = { from: '0x0', to: minter, value: amountInt }; + ctx.stub.setEvent('Transfer', Buffer.from(JSON.stringify(transferEvent))); + + console.log(`minter account ${minter} balance updated from ${currentBalance} to ${updatedBalance}`); + return true; + } + + /** + * Burn redeem tokens from minter's account balance + * + * @param {Context} ctx the transaction context + * @param {Integer} amount amount of tokens to be burned + * @returns {Object} The balance + */ + async Burn(ctx, amount) { + + // Check minter authorization - this sample assumes Org1 is the central banker with privilege to burn tokens + const clientMSPID = ctx.clientIdentity.getMSPID(); + if (clientMSPID !== 'Org1MSP') { + throw new Error('client is not authorized to mint new tokens'); + } + + const minter = ctx.clientIdentity.getID(); + + const amountInt = parseInt(amount); + + const balanceKey = ctx.stub.createCompositeKey(balancePrefix, [minter]); + + const currentBalanceBytes = await ctx.stub.getState(balanceKey); + if (!currentBalanceBytes || currentBalanceBytes.length === 0) { + throw new Error('The balance does not exist'); + } + const currentBalance = parseInt(currentBalanceBytes.toString()); + const updatedBalance = currentBalance - amountInt; + + await ctx.stub.putState(balanceKey, Buffer.from(updatedBalance.toString())); + + // Decrease totalSupply + const totalSupplyBytes = await ctx.stub.getState(totalSupplyKey); + if (!totalSupplyBytes || totalSupplyBytes.length === 0) { + throw new Error('totalSupply does not exist.'); + } + const totalSupply = parseInt(totalSupplyBytes.toString()) - amountInt; + await ctx.stub.putState(totalSupplyKey, Buffer.from(totalSupply.toString())); + + // Emit the Transfer event + const transferEvent = { from: minter, to: '0x0', value: amountInt }; + ctx.stub.setEvent('Transfer', Buffer.from(JSON.stringify(transferEvent))); + + console.log(`minter account ${minter} balance updated from ${currentBalance} to ${updatedBalance}`); + return true; + } + + /** + * ClientAccountBalance returns the balance of the requesting client's account. + * + * @param {Context} ctx the transaction context + * @returns {Number} Returns the account balance + */ + async ClientAccountBalance(ctx) { + // Get ID of submitting client identity + const clientAccountID = ctx.clientIdentity.getID(); + + const balanceKey = ctx.stub.createCompositeKey(balancePrefix, [clientAccountID]); + const balanceBytes = await ctx.stub.getState(balanceKey); + if (!balanceBytes || balanceBytes.length === 0) { + throw new Error(`the account ${clientAccountID} does not exist`); + } + const balance = parseInt(balanceBytes.toString()); + + return balance; + } + + // ClientAccountID returns the id of the requesting client's account. + // In this implementation, the client account ID is the clientId itself. + // Users can use this function to get their own account id, which they can then give to others as the payment address + async ClientAccountID(ctx) { + // Get ID of submitting client identity + const clientAccountID = ctx.clientIdentity.getID(); + return clientAccountID; + } + +} + +module.exports = TokenERC20Contract; \ No newline at end of file diff --git a/token-account-based/chaincode-javascript/package.json b/token-account-based/chaincode-javascript/package.json new file mode 100644 index 00000000..e704f980 --- /dev/null +++ b/token-account-based/chaincode-javascript/package.json @@ -0,0 +1,51 @@ +{ + "name": "token-erc20", + "version": "0.0.1", + "description": "Token-ERC20 contract implemented in JavaScript", + "main": "index.js", + "engines": { + "node": ">=12", + "npm": ">=5" + }, + "scripts": { + "lint": "eslint .", + "pretest": "npm run lint", + "test": "nyc mocha --recursive", + "mocha": "mocha --recursive", + "start": "fabric-chaincode-node start" + }, + "engineStrict": true, + "author": "Hyperledger", + "license": "Apache-2.0", + "dependencies": { + "fabric-contract-api": "^2.0.0", + "fabric-shim": "^2.0.0" + }, + "devDependencies": { + "chai": "^4.1.2", + "chai-as-promised": "^7.1.1", + "eslint": "^4.19.1", + "mocha": "^8.0.1", + "nyc": "^14.1.1", + "sinon": "^6.0.0", + "sinon-chai": "^3.2.0" + }, + "nyc": { + "exclude": [ + "coverage/**", + "test/**", + "index.js", + ".eslintrc.js" + ], + "reporter": [ + "text-summary", + "html" + ], + "all": true, + "check-coverage": false, + "statements": 100, + "branches": 100, + "functions": 100, + "lines": 100 + } +} diff --git a/token-account-based/chaincode-javascript/test/tokenERC20.test.js b/token-account-based/chaincode-javascript/test/tokenERC20.test.js new file mode 100644 index 00000000..6cfafa58 --- /dev/null +++ b/token-account-based/chaincode-javascript/test/tokenERC20.test.js @@ -0,0 +1,271 @@ +/* +SPDX-License-Identifier: Apache-2.0 +*/ + +'use strict'; + +const { Context } = require('fabric-contract-api'); +const { ChaincodeStub, ClientIdentity } = require('fabric-shim'); + +const { TokenERC20Contract } = require('..'); + +const chai = require('chai'); +const chaiAsPromised = require('chai-as-promised'); +const sinon = require('sinon'); +const expect = chai.expect; + +chai.should(); +chai.use(chaiAsPromised); + +describe('Chaincode', () => { + let sandbox; + let token; + let ctx; + let mockStub; + let mockClientIdentity; + + beforeEach('Sandbox creation', () => { + sandbox = sinon.createSandbox(); + token = new TokenERC20Contract('token-erc20'); + + ctx = sinon.createStubInstance(Context); + mockStub = sinon.createStubInstance(ChaincodeStub); + ctx.stub = mockStub; + mockClientIdentity = sinon.createStubInstance(ClientIdentity); + ctx.clientIdentity = mockClientIdentity; + + mockStub.putState.resolves('some state'); + mockStub.setEvent.returns('set event'); + + }); + + afterEach('Sandbox restoration', () => { + sandbox.restore(); + }); + + describe('#TokenName', () => { + it('should work', async () => { + mockStub.getState.resolves('some state'); + + const response = await token.TokenName(ctx); + sinon.assert.calledWith(mockStub.getState, 'name'); + expect(response).to.equals('some state'); + }); + }); + + describe('#Symbol', () => { + it('should work', async () => { + mockStub.getState.resolves('some state'); + + const response = await token.Symbol(ctx); + sinon.assert.calledWith(mockStub.getState, 'symbol'); + expect(response).to.equals('some state'); + }); + }); + + describe('#Decimals', () => { + it('should work', async () => { + mockStub.getState.resolves(Buffer.from('2')); + + const response = await token.Decimals(ctx); + sinon.assert.calledWith(mockStub.getState, 'decimals'); + expect(response).to.equals(2); + }); + }); + + describe('#TotalSupply', () => { + it('should work', async () => { + mockStub.getState.resolves(Buffer.from('10000')); + + const response = await token.TotalSupply(ctx); + sinon.assert.calledWith(mockStub.getState, 'totalSupply'); + expect(response).to.equals(10000); + }); + }); + + describe('#BalanceOf', () => { + it('should work', async () => { + mockStub.createCompositeKey.returns('balance_Alice'); + mockStub.getState.resolves(Buffer.from('1000')); + + const response = await token.BalanceOf(ctx, 'Alice'); + expect(response).to.equals(1000); + }); + }); + + describe('#_transfer', () => { + it('should fail when the sender does not have enough token', async () => { + mockStub.createCompositeKey.withArgs('balance', ['Alice']).returns('balance_Alice'); + mockStub.getState.withArgs('balance_Alice').resolves(Buffer.from('500')); + + await expect(token._transfer(ctx, 'Alice', 'Bob', '1000')) + .to.be.rejectedWith(Error, 'client account Alice has insufficient funds.'); + }); + + it('should transfer to a new account when the sender has enough token', async () => { + mockStub.createCompositeKey.withArgs('balance', ['Alice']).returns('balance_Alice'); + mockStub.getState.withArgs('balance_Alice').resolves(Buffer.from('1000')); + + mockStub.createCompositeKey.withArgs('balance', ['Bob']).returns('balance_Bob'); + mockStub.getState.withArgs('balance_Bob').resolves(null); + + const response = await token._transfer(ctx, 'Alice', 'Bob', '1000'); + sinon.assert.calledWith(mockStub.putState.getCall(0), 'balance_Alice', Buffer.from('0')); + sinon.assert.calledWith(mockStub.putState.getCall(1), 'balance_Bob', Buffer.from('1000')); + expect(response).to.equals(true); + }); + + it('should transfer to the existing account when the sender has enough token', async () => { + mockStub.createCompositeKey.withArgs('balance', ['Alice']).returns('balance_Alice'); + mockStub.getState.withArgs('balance_Alice').resolves(Buffer.from('1000')); + + mockStub.createCompositeKey.withArgs('balance', ['Bob']).returns('balance_Bob'); + mockStub.getState.withArgs('balance_Bob').resolves(Buffer.from('2000')); + + const response = await token._transfer(ctx, 'Alice', 'Bob', '1000'); + sinon.assert.calledWith(mockStub.putState.getCall(0), 'balance_Alice', Buffer.from('0')); + sinon.assert.calledWith(mockStub.putState.getCall(1), 'balance_Bob', Buffer.from('3000')); + expect(response).to.equals(true); + }); + + }); + + describe('#Transfer', () => { + it('should work', async () => { + mockClientIdentity.getID.returns('Alice'); + sinon.stub(token, '_transfer').returns(true); + + const response = await token.Transfer(ctx, 'Bob', '1000'); + const event = { from: 'Alice', to: 'Bob', value: 1000 }; + sinon.assert.calledWith(mockStub.setEvent, 'Transfer', Buffer.from(JSON.stringify(event))); + expect(response).to.equals(true); + }); + }); + + describe('#TransferFrom', () => { + it('should fail when the spender is not allowed to spend the token', async () => { + mockClientIdentity.getID.returns('Charlie'); + + mockStub.createCompositeKey.withArgs('allowance', ['Alice', 'Charlie']).returns('allowance_Alice_Charlie'); + mockStub.getState.withArgs('allowance_Alice_Charlie').resolves(Buffer.from('0')); + + await expect(token.TransferFrom(ctx, 'Alice', 'Bob', '1000')) + .to.be.rejectedWith(Error, 'The spender does not have enough allowance to spend.'); + }); + + it('should transfer when the spender is allowed to spend the token', async () => { + mockClientIdentity.getID.returns('Charlie'); + + mockStub.createCompositeKey.withArgs('allowance', ['Alice', 'Charlie']).returns('allowance_Alice_Charlie'); + mockStub.getState.withArgs('allowance_Alice_Charlie').resolves(Buffer.from('3000')); + + sinon.stub(token, '_transfer').returns(true); + + const response = await token.TransferFrom(ctx, 'Alice', 'Bob', '1000'); + sinon.assert.calledWith(mockStub.putState, 'allowance_Alice_Charlie', Buffer.from('2000')); + const event = { from: 'Alice', to: 'Bob', value: 1000 }; + sinon.assert.calledWith(mockStub.setEvent, 'Transfer', Buffer.from(JSON.stringify(event))); + expect(response).to.equals(true); + }); + }); + + describe('#Approve', () => { + it('should work', async () => { + mockClientIdentity.getID.returns('Dave'); + mockStub.createCompositeKey.returns('allowance_Dave_Eve'); + + const response = await token.Approve(ctx, 'Ellen', '1000'); + sinon.assert.calledWith(mockStub.putState, 'allowance_Dave_Eve', Buffer.from('1000')); + expect(response).to.equals(true); + }); + }); + + describe('#Allowance', () => { + it('should work', async () => { + mockStub.createCompositeKey.returns('allowance_Dave_Eve'); + mockStub.getState.resolves(Buffer.from('1000')); + + const response = await token.Allowance(ctx, 'Dave', 'Eve'); + expect(response).to.equals(1000); + }); + }); + + describe('#Mint', () => { + it('should add token to a new account and a new total supply', async () => { + mockClientIdentity.getMSPID.returns('Org1MSP'); + mockClientIdentity.getID.returns('Alice'); + mockStub.createCompositeKey.returns('balance_Alice'); + mockStub.getState.withArgs('balance_Alice').resolves(null); + mockStub.getState.withArgs('totalSupply').resolves(null); + + const response = await token.Mint(ctx, '1000'); + sinon.assert.calledWith(mockStub.putState.getCall(0), 'balance_Alice', Buffer.from('1000')); + sinon.assert.calledWith(mockStub.putState.getCall(1), 'totalSupply', Buffer.from('1000')); + expect(response).to.equals(true); + }); + + it('should add token to the existing account and the existing total supply', async () => { + mockClientIdentity.getMSPID.returns('Org1MSP'); + mockClientIdentity.getID.returns('Alice'); + mockStub.createCompositeKey.returns('balance_Alice'); + mockStub.getState.withArgs('balance_Alice').resolves(Buffer.from('1000')); + mockStub.getState.withArgs('totalSupply').resolves(Buffer.from('2000')); + + const response = await token.Mint(ctx, '1000'); + sinon.assert.calledWith(mockStub.putState.getCall(0), 'balance_Alice', Buffer.from('2000')); + sinon.assert.calledWith(mockStub.putState.getCall(1), 'totalSupply', Buffer.from('3000')); + expect(response).to.equals(true); + }); + + it('should add token to a new account and the existing total supply', async () => { + mockClientIdentity.getMSPID.returns('Org1MSP'); + mockClientIdentity.getID.returns('Alice'); + mockStub.createCompositeKey.returns('balance_Alice'); + mockStub.getState.withArgs('balance_Alice').resolves(null); + mockStub.getState.withArgs('totalSupply').resolves(Buffer.from('2000')); + + const response = await token.Mint(ctx, '1000'); + sinon.assert.calledWith(mockStub.putState.getCall(0), 'balance_Alice', Buffer.from('1000')); + sinon.assert.calledWith(mockStub.putState.getCall(1), 'totalSupply', Buffer.from('3000')); + expect(response).to.equals(true); + }); + + }); + + describe('#Burn', () => { + it('should work', async () => { + mockClientIdentity.getMSPID.returns('Org1MSP'); + mockClientIdentity.getID.returns('Alice'); + mockStub.createCompositeKey.returns('balance_Alice'); + mockStub.getState.withArgs('balance_Alice').resolves(Buffer.from('1000')); + mockStub.getState.withArgs('totalSupply').resolves(Buffer.from('2000')); + + const response = await token.Burn(ctx, '1000'); + sinon.assert.calledWith(mockStub.putState.getCall(0), 'balance_Alice', Buffer.from('0')); + sinon.assert.calledWith(mockStub.putState.getCall(1), 'totalSupply', Buffer.from('1000')); + expect(response).to.equals(true); + }); + }); + + describe('#ClientAccountBalance', () => { + it('should work', async () => { + mockClientIdentity.getID.returns('Alice'); + mockStub.createCompositeKey.returns('balance_Alice'); + mockStub.getState.resolves(Buffer.from('1000')); + + const response = await token.ClientAccountBalance(ctx,); + expect(response).to.equals(1000); + }); + }); + + describe('#ClientAccountID', () => { + it('should work', async () => { + mockClientIdentity.getID.returns('x509::{subject DN}::{issuer DN}'); + + const response = await token.ClientAccountID(ctx); + sinon.assert.calledOnce(mockClientIdentity.getID); + expect(response).to.equals('x509::{subject DN}::{issuer DN}'); + }); + }); + +}); From 0088222df5b31bb95965fa0c9254b4ba12bd52a3 Mon Sep 17 00:00:00 2001 From: nikhil550 Date: Wed, 28 Oct 2020 17:00:24 -0400 Subject: [PATCH 27/38] Move high-throughput sample to go application (#364) Signed-off-by: Nikhil Gupta high throughput app --- high-throughput/README.md | 84 ++++--- high-throughput/application-go/.gitignore | 4 + high-throughput/application-go/app.go | 70 ++++++ .../application-go/functions/deletePrune.go | 69 ++++++ .../application-go/functions/manyUpdates.go | 86 +++++++ .../application-go/functions/query.go | 69 ++++++ .../application-go/functions/update.go | 74 ++++++ .../application-go/functions/util.go | 55 +++++ high-throughput/application-go/go.mod | 5 + high-throughput/application-go/go.sum | 224 ++++++++++++++++++ .../{chaincode => chaincode-go}/go.mod | 0 .../{chaincode => chaincode-go}/go.sum | 0 .../high-throughput.go | 0 high-throughput/networkDown.sh | 6 +- .../scripts/check-commit-readiness.sh | 64 ----- high-throughput/scripts/del-traditional.sh | 9 - high-throughput/scripts/delete-invoke.sh | 9 - high-throughput/scripts/get-invoke.sh | 9 - high-throughput/scripts/get-traditional.sh | 9 - .../scripts/many-updates-traditional.sh | 12 - high-throughput/scripts/many-updates.sh | 12 - high-throughput/scripts/prune-invoke.sh | 9 - high-throughput/scripts/setenv.sh | 8 - high-throughput/scripts/update-invoke.sh | 9 - high-throughput/startFabric.sh | 2 +- 25 files changed, 718 insertions(+), 180 deletions(-) create mode 100755 high-throughput/application-go/.gitignore create mode 100644 high-throughput/application-go/app.go create mode 100644 high-throughput/application-go/functions/deletePrune.go create mode 100644 high-throughput/application-go/functions/manyUpdates.go create mode 100644 high-throughput/application-go/functions/query.go create mode 100644 high-throughput/application-go/functions/update.go create mode 100644 high-throughput/application-go/functions/util.go create mode 100644 high-throughput/application-go/go.mod create mode 100644 high-throughput/application-go/go.sum rename high-throughput/{chaincode => chaincode-go}/go.mod (100%) rename high-throughput/{chaincode => chaincode-go}/go.sum (100%) rename high-throughput/{chaincode => chaincode-go}/high-throughput.go (100%) delete mode 100755 high-throughput/scripts/check-commit-readiness.sh delete mode 100644 high-throughput/scripts/del-traditional.sh delete mode 100755 high-throughput/scripts/delete-invoke.sh delete mode 100755 high-throughput/scripts/get-invoke.sh delete mode 100755 high-throughput/scripts/get-traditional.sh delete mode 100755 high-throughput/scripts/many-updates-traditional.sh delete mode 100755 high-throughput/scripts/many-updates.sh delete mode 100755 high-throughput/scripts/prune-invoke.sh delete mode 100644 high-throughput/scripts/setenv.sh delete mode 100755 high-throughput/scripts/update-invoke.sh diff --git a/high-throughput/README.md b/high-throughput/README.md index d952a23e..1314b56c 100644 --- a/high-throughput/README.md +++ b/high-throughput/README.md @@ -107,56 +107,86 @@ Change back into the `high-throughput` directory in `fabic-samples`. Start the n If successful, you will see messages of the Fabric test network being created and the chaincode being deployed, followed by the execution time of the script: ``` -Total setup execution time : 141 secs ... +Total setup execution time : 81 secs ... ``` The `high-throughput` chaincode is now ready to receive invocations. ### Invoke the chaincode -All invocations are provided as scripts in `scripts` folder. You can use these scripts to create and remove assets that you put on the ledger. + +You can invoke the `high-througput` chaincode using a Go application in the `application-go` folder. The Go application will allow us to submit many transactions to the network concurrently. Navigate to the application: +``` +cd application-go +``` #### Update -The format for update is: `./scripts/update-invoke.sh name value operation` where `name` is the name of the variable to update, `value` is the value to -add to the variable, and `operation` is either `+` or `-` depending on what type of operation you'd like to add to the variable. In the future, -multiply/divide operations will be supported (or add them yourself to the chaincode as an exercise!) +The format for update is: `go run app.go update name value operation` where `name` is the name of the variable to update, `value` is the value to add to the variable, and `operation` is either `+` or `-` depending on what type of operation you'd like to add to the variable. -Example: `./scripts/update-invoke.sh myvar 100 +` +Example: `go run app.go update myvar 100 +` -#### Get -The format for get is: `./get-invoke.sh name` where `name` is the name of the variable to get. +#### Query +You can query the value of a variable by running `go run app.go get name` where `name` is the name of the variable to get. -Example: `./scripts/get-invoke.sh myvar` +Example: `go run app.go get myvar` #### Prune -Pruning takes all the deltas generated for a variable and combines them all into a single row, deleting all previous rows. This helps cleanup -the ledger when many updates have been performed. +Pruning takes all the deltas generated for a variable and combines them all into a single row, deleting all previous rows. This helps cleanup the ledger when many updates have been performed. -The format for pruning is: `./scripts/prune-invoke.sh name` where `name` is the name of the variable to prune. +The format for pruning is: `go run app.go prune name` where `name` is the name of the variable to prune. -Example: `./scripts/prune-invoke.sh myvar` +Example: `go run app.go prune myvar` #### Delete -The format for delete is: `./delete-invoke.sh name` where `name` is the name of the variable to delete. +The format for delete is: `go run app.go delete name` where `name` is the name of the variable to delete. -Example: `./scripts/delete-invoke.sh myvar` +Example: `go run app.go delete myvar` ### Test the Network -Two scripts are provided to show the advantage of using this system when running many parallel transactions at once: `many-updates.sh` and -`many-updates-traditional.sh`. The first script accepts the same arguments as `update-invoke.sh` but duplicates the invocation 1000 times -and in parallel. The final value, therefore, should be the given update value * 1000. Run this script to confirm that your network is functioning -properly. You can confirm this by checking your peer and orderer logs and verifying that no invocations are rejected due to improper versions. -The second script, `many-updates-traditional.sh`, also sends 1000 transactions but using the traditional storage system. It'll update a single -row in the ledger 1000 times, with a value incrementing by one each time (i.e. the first invocation sets it to 0 and the last to 1000). The -expectation would be that the final value of the row is 999. However, the final value changes each time this script is run and you'll find -errors in the peer and orderer logs. +The application provides two methods that demonstrate the advantages of this system by submitting many concurrent transactions to the smart contract: `manyUpdates` and `manyUpdatesTraditional`. The first function accepts the same arguments as `update-invoke.sh` but runs the invocation 1000 times in parallel. The final value, therefore, should be the given update value * 1000. -There are two other scripts, `get-traditional.sh`, which simply gets the value of a row in the traditional way, with no deltas, and `del-traditional.sh` will delete an asset in the traditional way. +The second function, `manyUpdatesTraditional`, submits 1000 transactions that attempt to upddate the same key in the world state 1000 times. -Examples: -`./scripts/many-updates.sh testvar 100 +` --> final value from `./scripts/get-invoke.sh testvar` should be 100000 +Run the following command to create and update `testvar1` a 1000 times: +``` +go run app.go manyUpdates testvar1 100 + +``` -`./scripts/many-updates-traditional.sh testvar` --> final value from `./scripts/get-traditional.sh testvar` is undefined +The application will query the variable after submitting the transaction. The result should be `100000`. + +We will now see what happens when you try to run 1000 concurrent updates using a traditional transaction. Run the following command to create a variable named `testvar2`: +``` +go run app.go update testvar2 100 + +``` +The variable will have a value of 100: +``` +2020/10/27 18:01:45 Value of variable testvar2 : 100 +``` + +Now lets try to update `testvar2` 1000 times in parallel: +``` +go run app.go manyUpdatesTraditional testvar2 100 + +``` + +When the program ends, you may see that none of the updates succeeded. +``` +2020/10/27 18:03:15 Final value of variable testvar2 : 100 +``` + +The transactions failed because multiple transactions in each block updated the same key. Because of these transactions generated read/write conflicts, the transactions included in each block were rejected in the validation stage. + +You can can examine the peer logs to view the messages generated by the rejected blocks: + + +`docker logs peer0.org1.example.com +[...] +2020-10-28 17:37:58.746 UTC [gossip.privdata] StoreBlock -> INFO 2190 [mychannel] Received block [407] from buffer +2020-10-28 17:37:58.749 UTC [committer.txvalidator] Validate -> INFO 2191 [mychannel] Validated block [407] in 2ms +2020-10-28 17:37:58.750 UTC [validation] validateAndPrepareBatch -> WARN 2192 Block [407] Transaction index [0] TxId [b6b14cf988b0d7d35d4e0d7a0d2ae0c9f5569bc10ec5010f03a28c22694b8ef6] marked as invalid by state validator. Reason code [MVCC_READ_CONFLICT] +2020-10-28 17:37:58.750 UTC [validation] validateAndPrepareBatch -> WARN 2193 Block [407] Transaction index [1] TxId [9d7c4f6ff95a0f22e01d6ffeda261227752e78db43f2673ad4ea6f0fdace44d1] marked as invalid by state validator. Reason code [MVCC_READ_CONFLICT] +2020-10-28 17:37:58.750 UTC [validation] validateAndPrepareBatch -> WARN 2194 Block [407] Transaction index [2] TxId [9cc228b61d8841208feb6160254aee098b1b3a903f645e62cfa12222e6f52e65] marked as invalid by state validator. Reason code [MVCC_READ_CONFLICT] +2020-10-28 17:37:58.750 UTC [validation] validateAndPrepareBatch -> WARN 2195 Block [407] Transaction index [3] TxId [2ae78d363c30b5f3445f2b028ccac7cf821f1d5d5c256d8c17bd42f33178e2ed] marked as invalid by state validator. Reason code [MVCC_READ_CONFLICT] +``` ### Clean up diff --git a/high-throughput/application-go/.gitignore b/high-throughput/application-go/.gitignore new file mode 100755 index 00000000..e9fec12f --- /dev/null +++ b/high-throughput/application-go/.gitignore @@ -0,0 +1,4 @@ +wallet +!wallet/.gitkeep + +keystore diff --git a/high-throughput/application-go/app.go b/high-throughput/application-go/app.go new file mode 100644 index 00000000..100d7c8e --- /dev/null +++ b/high-throughput/application-go/app.go @@ -0,0 +1,70 @@ +/* +Copyright 2020 IBM All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package main + +import ( + "log" + "os" + + f "github.com/hyperledger/fabric-samples/high-throughput/application-go/functions" +) + +func main() { + + var function, variableName, change, sign string + + if len(os.Args) <= 2 { + log.Println("Usage: function variableName") + log.Fatalf("functions: update manyUpdates manyUpdatesTraditional get prune delete") + } else if (os.Args[1] == "update" || os.Args[1] == "manyUpdates" || os.Args[1] == "manyUpdatesTraditional") && len(os.Args) < 5 { + log.Fatalf("error: provide value and operation") + } else if len(os.Args) == 3 { + function = os.Args[1] + variableName = os.Args[2] + } else if len(os.Args) == 5 { + function = os.Args[1] + variableName = os.Args[2] + change = os.Args[3] + sign = os.Args[4] + } + + // Handle different functions + if function == "update" { + result, err := f.Update(function, variableName, change, sign) + if err != nil { + log.Fatalf("error: %v", err) + } + log.Println("Value of variable", string(variableName), ": ", string(result)) + + } else if function == "delete" || function == "prune" || function == "delstandard" { + result, err := f.DeletePrune(function, variableName) + if err != nil { + log.Fatalf("error: %v", err) + } + log.Println(string(result)) + } else if function == "get" || function == "getstandard" { + result, err := f.Query(function, variableName) + if err != nil { + log.Fatalf("error: %v", err) + } + log.Println("Value of variable", string(variableName), ": ", string(result)) + } else if function == "manyUpdates" { + log.Println("submitting 1000 concurrent updates...") + result, err := f.ManyUpdates("update", variableName, change, sign) + if err != nil { + log.Fatalf("error: %v", err) + } + log.Println("Final value of variable", string(variableName), ": ", string(result)) + } else if function == "manyUpdatesTraditional" { + log.Println("submitting 1000 concurrent updates...") + result, err := f.ManyUpdates("putstandard", variableName, change, sign) + if err != nil { + log.Fatalf("error: %v", err) + } + log.Println("Final value of variable", string(variableName), ": ", string(result)) + } +} diff --git a/high-throughput/application-go/functions/deletePrune.go b/high-throughput/application-go/functions/deletePrune.go new file mode 100644 index 00000000..12492660 --- /dev/null +++ b/high-throughput/application-go/functions/deletePrune.go @@ -0,0 +1,69 @@ +/* +Copyright 2020 IBM All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package functions + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/hyperledger/fabric-sdk-go/pkg/core/config" + "github.com/hyperledger/fabric-sdk-go/pkg/gateway" +) + +// DeletePrune deletes or prunes a variable +func DeletePrune(function, variableName string) ([]byte, error) { + + err := os.Setenv("DISCOVERY_AS_LOCALHOST", "true") + if err != nil { + return nil, fmt.Errorf("error setting DISCOVERY_AS_LOCALHOST environemnt variable: %v", err) + } + + wallet, err := gateway.NewFileSystemWallet("wallet") + if err != nil { + return nil, fmt.Errorf("failed to create wallet: %v", err) + } + + if !wallet.Exists("appUser") { + err := populateWallet(wallet) + if err != nil { + return nil, fmt.Errorf("failed to populate wallet contents: %v", err) + } + } + + ccpPath := filepath.Join( + "..", + "..", + "test-network", + "organizations", + "peerOrganizations", + "org1.example.com", + "connection-org1.yaml", + ) + + gw, err := gateway.Connect( + gateway.WithConfig(config.FromFile(filepath.Clean(ccpPath))), + gateway.WithIdentity(wallet, "appUser"), + ) + if err != nil { + return nil, fmt.Errorf("failed to connect to gateway: %v", err) + } + defer gw.Close() + + network, err := gw.GetNetwork("mychannel") + if err != nil { + return nil, fmt.Errorf("failed to get network: %v", err) + } + + contract := network.GetContract("bigdatacc") + + result, err := contract.SubmitTransaction(function, variableName) + if err != nil { + return result, fmt.Errorf("failed to Submit transaction: %v", err) + } + return result, err +} diff --git a/high-throughput/application-go/functions/manyUpdates.go b/high-throughput/application-go/functions/manyUpdates.go new file mode 100644 index 00000000..521b2e47 --- /dev/null +++ b/high-throughput/application-go/functions/manyUpdates.go @@ -0,0 +1,86 @@ +/* +Copyright 2020 IBM All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package functions + +import ( + "fmt" + "os" + "path/filepath" + "sync" + + "github.com/hyperledger/fabric-sdk-go/pkg/core/config" + "github.com/hyperledger/fabric-sdk-go/pkg/gateway" +) + +// ManyUpdates allows you to push many cuncurrent updates to a variable +func ManyUpdates(function, variableName, change, sign string) ([]byte, error) { + + err := os.Setenv("DISCOVERY_AS_LOCALHOST", "true") + if err != nil { + return nil, fmt.Errorf("error setting DISCOVERY_AS_LOCALHOST environemnt variable: %v", err) + } + + wallet, err := gateway.NewFileSystemWallet("wallet") + if err != nil { + return nil, fmt.Errorf("failed to create wallet: %v", err) + } + + if !wallet.Exists("appUser") { + err := populateWallet(wallet) + if err != nil { + return nil, fmt.Errorf("failed to populate wallet contents: %v", err) + } + } + + ccpPath := filepath.Join( + "..", + "..", + "test-network", + "organizations", + "peerOrganizations", + "org1.example.com", + "connection-org1.yaml", + ) + + gw, err := gateway.Connect( + gateway.WithConfig(config.FromFile(filepath.Clean(ccpPath))), + gateway.WithIdentity(wallet, "appUser"), + ) + if err != nil { + return nil, fmt.Errorf("failed to connect to gateway: %v", err) + } + defer gw.Close() + + network, err := gw.GetNetwork("mychannel") + if err != nil { + return nil, fmt.Errorf("failed to get network: %v", err) + } + + contract := network.GetContract("bigdatacc") + + var wg sync.WaitGroup + + for i := 0; i < 1000; i++ { + wg.Add(1) + go func() ([]byte, error) { + defer wg.Done() + result, err := contract.SubmitTransaction(function, variableName, change, sign) + if err != nil { + return result, fmt.Errorf("failed to evaluate transaction: %v", err) + } + return result, nil + }() + } + + wg.Wait() + + result, err := contract.EvaluateTransaction("get", variableName) + if err != nil { + return nil, fmt.Errorf("failed to evaluate transaction: %v", err) + } + return result, err +} diff --git a/high-throughput/application-go/functions/query.go b/high-throughput/application-go/functions/query.go new file mode 100644 index 00000000..94716524 --- /dev/null +++ b/high-throughput/application-go/functions/query.go @@ -0,0 +1,69 @@ +/* +Copyright 2020 IBM All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package functions + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/hyperledger/fabric-sdk-go/pkg/core/config" + "github.com/hyperledger/fabric-sdk-go/pkg/gateway" +) + +// Query can be used to read the latest value of a variable +func Query(function, variableName string) ([]byte, error) { + + err := os.Setenv("DISCOVERY_AS_LOCALHOST", "true") + if err != nil { + return nil, fmt.Errorf("error setting DISCOVERY_AS_LOCALHOST environemnt variable: %v", err) + } + + wallet, err := gateway.NewFileSystemWallet("wallet") + if err != nil { + return nil, fmt.Errorf("failed to create wallet: %v", err) + } + + if !wallet.Exists("appUser") { + err = populateWallet(wallet) + if err != nil { + return nil, fmt.Errorf("failed to populate wallet contents: %v", err) + } + } + + ccpPath := filepath.Join( + "..", + "..", + "test-network", + "organizations", + "peerOrganizations", + "org1.example.com", + "connection-org1.yaml", + ) + + gw, err := gateway.Connect( + gateway.WithConfig(config.FromFile(filepath.Clean(ccpPath))), + gateway.WithIdentity(wallet, "appUser"), + ) + if err != nil { + return nil, fmt.Errorf("failed to connect to gateway: %v", err) + } + defer gw.Close() + + network, err := gw.GetNetwork("mychannel") + if err != nil { + return nil, fmt.Errorf("failed to get network: %v", err) + } + + contract := network.GetContract("bigdatacc") + + result, err := contract.EvaluateTransaction(function, variableName) + if err != nil { + return nil, fmt.Errorf("failed to evaluate transaction: %v", err) + } + return result, err +} diff --git a/high-throughput/application-go/functions/update.go b/high-throughput/application-go/functions/update.go new file mode 100644 index 00000000..eb77cd5d --- /dev/null +++ b/high-throughput/application-go/functions/update.go @@ -0,0 +1,74 @@ +/* +Copyright 2020 IBM All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package functions + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/hyperledger/fabric-sdk-go/pkg/core/config" + "github.com/hyperledger/fabric-sdk-go/pkg/gateway" +) + +// Update can be used to update or prune the variable +func Update(function, variableName, change, sign string) ([]byte, error) { + + err := os.Setenv("DISCOVERY_AS_LOCALHOST", "true") + if err != nil { + return nil, fmt.Errorf("error setting DISCOVERY_AS_LOCALHOST environemnt variable: %v", err) + } + + wallet, err := gateway.NewFileSystemWallet("wallet") + if err != nil { + return nil, fmt.Errorf("failed to create wallet: %v", err) + } + + if !wallet.Exists("appUser") { + err := populateWallet(wallet) + if err != nil { + return nil, fmt.Errorf("failed to populate wallet contents: %v", err) + } + } + + ccpPath := filepath.Join( + "..", + "..", + "test-network", + "organizations", + "peerOrganizations", + "org1.example.com", + "connection-org1.yaml", + ) + + gw, err := gateway.Connect( + gateway.WithConfig(config.FromFile(filepath.Clean(ccpPath))), + gateway.WithIdentity(wallet, "appUser"), + ) + if err != nil { + return nil, fmt.Errorf("failed to connect to gateway: %v", err) + } + defer gw.Close() + + network, err := gw.GetNetwork("mychannel") + if err != nil { + return nil, fmt.Errorf("failed to get network: %v", err) + } + + contract := network.GetContract("bigdatacc") + + result, err := contract.SubmitTransaction(function, variableName, change, sign) + if err != nil { + return result, fmt.Errorf("failed to Submit transaction: %v", err) + } + + result, err = contract.EvaluateTransaction("get", variableName) + if err != nil { + return nil, fmt.Errorf("failed to evaluate transaction: %v", err) + } + return result, err +} diff --git a/high-throughput/application-go/functions/util.go b/high-throughput/application-go/functions/util.go new file mode 100644 index 00000000..3447f57e --- /dev/null +++ b/high-throughput/application-go/functions/util.go @@ -0,0 +1,55 @@ +/* +Copyright 2020 IBM All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package functions + +import ( + "fmt" + "io/ioutil" + "path/filepath" + + "github.com/hyperledger/fabric-sdk-go/pkg/gateway" +) + +func populateWallet(wallet *gateway.Wallet) error { + credPath := filepath.Join( + "..", + "..", + "test-network", + "organizations", + "peerOrganizations", + "org1.example.com", + "users", + "User1@org1.example.com", + "msp", + ) + + certPath := filepath.Join(credPath, "signcerts", "cert.pem") + // read the certificate pem + cert, err := ioutil.ReadFile(filepath.Clean(certPath)) + if err != nil { + return err + } + + keyDir := filepath.Join(credPath, "keystore") + // there's a single file in this dir containing the private key + files, err := ioutil.ReadDir(keyDir) + if err != nil { + return err + } + if len(files) != 1 { + return fmt.Errorf("keystore folder should have contain one file") + } + keyPath := filepath.Join(keyDir, files[0].Name()) + key, err := ioutil.ReadFile(filepath.Clean(keyPath)) + if err != nil { + return err + } + + identity := gateway.NewX509Identity("Org1MSP", string(cert), string(key)) + + return wallet.Put("appUser", identity) +} diff --git a/high-throughput/application-go/go.mod b/high-throughput/application-go/go.mod new file mode 100644 index 00000000..1b2dcd25 --- /dev/null +++ b/high-throughput/application-go/go.mod @@ -0,0 +1,5 @@ +module github.com/hyperledger/fabric-samples/high-throughput/application-go + +go 1.14 + +require github.com/hyperledger/fabric-sdk-go v1.0.0-beta3.0.20201006151309-9c426dcc5096 diff --git a/high-throughput/application-go/go.sum b/high-throughput/application-go/go.sum new file mode 100644 index 00000000..506e0a6b --- /dev/null +++ b/high-throughput/application-go/go.sum @@ -0,0 +1,224 @@ +bitbucket.org/liamstask/goose v0.0.0-20150115234039-8488cc47d90c/go.mod h1:hSVuE3qU7grINVSwrmzHfpg9k87ALBk+XaualNyUzI4= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0= +github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0= +github.com/Knetic/govaluate v3.0.0+incompatible h1:7o6+MAPhYTCF0+fdvoz1xDedhRb4f6s9Tn1Tt7/WTEg= +github.com/Knetic/govaluate v3.0.0+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= +github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= +github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/certifi/gocertifi v0.0.0-20180118203423-deb3ae2ef261/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudflare/backoff v0.0.0-20161212185259-647f3cdfc87a/go.mod h1:rzgs2ZOiguV6/NpiDgADjRLPNyZlApIWxKpkT+X8SdY= +github.com/cloudflare/cfssl v1.4.1 h1:vScfU2DrIUI9VPHBVeeAQ0q5A+9yshO1Gz+3QoUQiKw= +github.com/cloudflare/cfssl v1.4.1/go.mod h1:KManx/OJPb5QY+y0+o/898AMcM128sF0bURvoVUSjTo= +github.com/cloudflare/go-metrics v0.0.0-20151117154305-6a9aea36fb41/go.mod h1:eaZPlJWD+G9wseg1BuRXlHnjntPMrywMsyxf+LTOdP4= +github.com/cloudflare/redoctober v0.0.0-20171127175943-746a508df14c/go.mod h1:6Se34jNoqrd8bTxrmJB2Bg2aoZ2CdSXonils9NsiNgo= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/getsentry/raven-go v0.0.0-20180121060056-563b81fc02b7/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= +github.com/go-kit/kit v0.8.0 h1:Wz+5lgoB0kkuqLEc6NVmwRknTKP6dTGbSqvhZtBI/j0= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0 h1:MP4Eh7ZCb31lleYCFuwm0oe4/YGak+5l1vA2NOE80nA= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-sql-driver/mysql v1.3.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.4.3 h1:GV+pQPG/EUUbkh47niozDcADz6go/dUwhVzdUQHIVRw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/google/certificate-transparency-go v1.0.21 h1:Yf1aXowfZ2nuboBsg7iYGLmwsOARdV86pfH3g95wXmE= +github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/hyperledger/fabric-config v0.0.5 h1:khRkm8U9Ghdg8VmZfptgzCFlCzrka8bPfUkM+/j6Zlg= +github.com/hyperledger/fabric-config v0.0.5/go.mod h1:YpITBI/+ZayA3XWY5lF302K7PAsFYjEEPM/zr3hegA8= +github.com/hyperledger/fabric-lib-go v1.0.0 h1:UL1w7c9LvHZUSkIvHTDGklxFv2kTeva1QI2emOVc324= +github.com/hyperledger/fabric-lib-go v1.0.0/go.mod h1:H362nMlunurmHwkYqR5uHL2UDWbQdbfz74n8kbCFsqc= +github.com/hyperledger/fabric-protos-go v0.0.0-20200424173316-dd554ba3746e/go.mod h1:xVYTjK4DtZRBxZ2D9aE4y6AbLaPwue2o/criQyQbVD0= +github.com/hyperledger/fabric-protos-go v0.0.0-20200707132912-fee30f3ccd23 h1:SEbB3yH4ISTGRifDamYXAst36gO2kM855ndMJlsv+pc= +github.com/hyperledger/fabric-protos-go v0.0.0-20200707132912-fee30f3ccd23/go.mod h1:xVYTjK4DtZRBxZ2D9aE4y6AbLaPwue2o/criQyQbVD0= +github.com/hyperledger/fabric-sdk-go v1.0.0-beta3.0.20201006151309-9c426dcc5096 h1:veml7LmfavSHqF8w8z/PGGlfdXvmx5SstQIH6Nyy87c= +github.com/hyperledger/fabric-sdk-go v1.0.0-beta3.0.20201006151309-9c426dcc5096/go.mod h1:qWE9Syfg1KbwNjtILk70bJLilnmCvllIYFCSY/pa1RU= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jmhodges/clock v0.0.0-20160418191101-880ee4c33548/go.mod h1:hGT6jSUVzF6no3QaDSMLGLEHtHSBSefs+MgcDWnmhmo= +github.com/jmoiron/sqlx v0.0.0-20180124204410-05cef0741ade/go.mod h1:IiEW3SEiiErVyFdH8NTuWjSifiEQKUoyK3LNqr2kCHU= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/sqlstruct v0.0.0-20150923205031-648daed35d49/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE= +github.com/kisom/goutils v1.1.0/go.mod h1:+UBTfd78habUYWFbNWTJNG+jNG/i/lGURakr4A/yNRw= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kylelemons/go-gypsy v0.0.0-20160905020020-08cad365cd28/go.mod h1:T/T7jsxVqf9k/zYOqbgNAsANsjxTd1Yq3htjDhQ1H0c= +github.com/lib/pq v0.0.0-20180201184707-88edab080323/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= +github.com/mitchellh/mapstructure v1.3.2 h1:mRS76wmkOn3KkKAyXDu42V+6ebnXWIztFSYGN7GeoRg= +github.com/mitchellh/mapstructure v1.3.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= +github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= +github.com/pelletier/go-toml v1.8.0 h1:Keo9qb7iRJs2voHvunFtuuYFsbWeOBh8/P9v/kVMFtw= +github.com/pelletier/go-toml v1.8.0/go.mod h1:D6yutnOGMveHEPV7VQOuvI/gXY61bv+9bAOTRnLElKs= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.1.0 h1:BQ53HtBmfOitExawJ6LokA4x8ov/z0SYYb0+HxJfRI8= +github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.6.0 h1:kRhiuYSXR3+uv2IbVbZhUxK5zVD/2pp3Gd2PpvPkpEo= +github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.3 h1:CTwfnzjQ+8dS6MhHHu4YswVAD99sL2wjPqP+VkURmKE= +github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/spf13/afero v1.3.1 h1:GPTpEAuNr98px18yNQ66JllNil98wfRZ/5Ukny8FeQA= +github.com/spf13/afero v1.3.1/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= +github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.1.1 h1:/8JBRFO4eoHu1TmpsLgNBq1CQgRUg4GolYlEFieqJgo= +github.com/spf13/viper v1.1.1/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= +github.com/weppos/publicsuffix-go v0.4.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k= +github.com/weppos/publicsuffix-go v0.5.0 h1:rutRtjBJViU/YjcI5d80t4JAVvDltS6bciJg2K1HrLU= +github.com/weppos/publicsuffix-go v0.5.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k= +github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= +github.com/zmap/rc2 v0.0.0-20131011165748-24b9757f5521/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE= +github.com/zmap/zcertificate v0.0.0-20180516150559-0e3d58b1bac4/go.mod h1:5iU54tB79AMBcySS0R2XIyZBAVmeHranShAFELYx7is= +github.com/zmap/zcrypto v0.0.0-20190729165852-9051775e6a2e h1:mvOa4+/DXStR4ZXOks/UsjeFdn5O5JpLUtzqk9U8xXw= +github.com/zmap/zcrypto v0.0.0-20190729165852-9051775e6a2e/go.mod h1:w7kd3qXHh8FNaczNjslXqvFQiv5mMWRXlL9klTUAHc8= +github.com/zmap/zlint v0.0.0-20190806154020-fd021b4cfbeb h1:vxqkjztXSaPVDc8FQCdHTaejm2x747f6yPbnu1h2xkg= +github.com/zmap/zlint v0.0.0-20190806154020-fd021b4cfbeb/go.mod h1:29UiAJNsiVdvTBFCJW8e3q6dcDbOoPkhMgttOSCIMMY= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d h1:1ZiEyfaQIg3Qh0EoqpwAakHVhecoE5wlSg5GjnafJGw= +golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980 h1:dfGZHvZk057jK2MCeWus/TowKpJ8y4AmooUzdBSR9GU= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3 h1:4y9KwBHBgBNwDbtu44R5o1fdOCQUEXhbk/P4A9WmJq0= +golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.29.1 h1:EC2SB8S04d2r73uptxphDSUG+kTKVgjRPF+N3xpxRB4= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/high-throughput/chaincode/go.mod b/high-throughput/chaincode-go/go.mod similarity index 100% rename from high-throughput/chaincode/go.mod rename to high-throughput/chaincode-go/go.mod diff --git a/high-throughput/chaincode/go.sum b/high-throughput/chaincode-go/go.sum similarity index 100% rename from high-throughput/chaincode/go.sum rename to high-throughput/chaincode-go/go.sum diff --git a/high-throughput/chaincode/high-throughput.go b/high-throughput/chaincode-go/high-throughput.go similarity index 100% rename from high-throughput/chaincode/high-throughput.go rename to high-throughput/chaincode-go/high-throughput.go diff --git a/high-throughput/networkDown.sh b/high-throughput/networkDown.sh index 5798f011..6e898f50 100755 --- a/high-throughput/networkDown.sh +++ b/high-throughput/networkDown.sh @@ -7,9 +7,11 @@ # Exit on first error set -ex -rm -rf bigdatacc.tar.gz log.txt - # Bring the test network down pushd ../test-network ./network.sh down popd + + +rm -rf application-go/wallet/ +rm -rf application-go/keystore/ \ No newline at end of file diff --git a/high-throughput/scripts/check-commit-readiness.sh b/high-throughput/scripts/check-commit-readiness.sh deleted file mode 100755 index 7d4b9c39..00000000 --- a/high-throughput/scripts/check-commit-readiness.sh +++ /dev/null @@ -1,64 +0,0 @@ -# -# Copyright IBM Corp All Rights Reserved -# -# SPDX-License-Identifier: Apache-2.0 -# - -setGlobals() { - ORG=$1 - if [ $ORG -eq 1 ]; then - CORE_PEER_LOCALMSPID="Org1MSP" - CORE_PEER_TLS_ROOTCERT_FILE=../test-network/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt - CORE_PEER_MSPCONFIGPATH=../test-network/organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp - CORE_PEER_ADDRESS=localhost:7051 - elif [ $ORG -eq 2 ]; then - CORE_PEER_LOCALMSPID="Org2MSP" - CORE_PEER_TLS_ROOTCERT_FILE=../test-network/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt - CORE_PEER_MSPCONFIGPATH=../test-network/organizations/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp - CORE_PEER_ADDRESS=localhost:9051 - else - echo "================== ERROR !!! ORG Unknown ==================" - fi - - if [ "$VERBOSE" == "true" ]; then - env | grep CORE - fi -} - -checkCommitReadiness() { - ORG=$1 - shift 3 - setGlobals $ORG - echo "===================== Simulating the commit of the chaincode definition on peer${PEER}.org${ORG} ===================== " - local rc=1 - local starttime=$(date +%s) - - # continue to poll - # we either get a successful response, or reach TIMEOUT - while - test "$(($(date +%s) - starttime))" -lt "$TIMEOUT" -a $rc -ne 0 - do - sleep $DELAY - echo "Attempting to check the commit readiness of the chaincode definition on peer0.org${ORG} ...$(($(date +%s) - starttime)) secs" - set -x - peer lifecycle chaincode checkcommitreadiness --channelID mychannel --name bigdatacc --signature-policy "OR('Org1MSP.peer', 'Org2MSP.peer')" --version 0 --init-required --sequence 1 >&log.txt - res=$? - { set +x; } 2>/dev/null - test $res -eq 0 || continue - let rc=0 - for var in "$@" - do - grep "$var" log.txt &>/dev/null || let rc=1 - done - done - echo - cat log.txt - if test $rc -eq 0; then - echo "===================== Checking the commit readiness of the chaincode definition successful on peer0.org${ORG} ===================== " - else - echo "!!!!!!!!!!!!!!! Check commit readiness result on peer0.org${ORG} is INVALID !!!!!!!!!!!!!!!!" - echo "================== ERROR !!! FAILED to execute End-2-End Scenario ==================" - echo - exit 1 - fi -} diff --git a/high-throughput/scripts/del-traditional.sh b/high-throughput/scripts/del-traditional.sh deleted file mode 100644 index e54d7aeb..00000000 --- a/high-throughput/scripts/del-traditional.sh +++ /dev/null @@ -1,9 +0,0 @@ -# -# Copyright IBM Corp All Rights Reserved -# -# SPDX-License-Identifier: Apache-2.0 -# - -source scripts/setenv.sh - -peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ../test-network/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n bigdatacc -c '{"Args":["delstandard","'$1'"]}' diff --git a/high-throughput/scripts/delete-invoke.sh b/high-throughput/scripts/delete-invoke.sh deleted file mode 100755 index ced25628..00000000 --- a/high-throughput/scripts/delete-invoke.sh +++ /dev/null @@ -1,9 +0,0 @@ -# -# Copyright IBM Corp All Rights Reserved -# -# SPDX-License-Identifier: Apache-2.0 -# - -source scripts/setenv.sh - -peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ../test-network/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n bigdatacc -c '{"Args":["delete","'$1'"]}' diff --git a/high-throughput/scripts/get-invoke.sh b/high-throughput/scripts/get-invoke.sh deleted file mode 100755 index 53375abf..00000000 --- a/high-throughput/scripts/get-invoke.sh +++ /dev/null @@ -1,9 +0,0 @@ -# -# Copyright IBM Corp All Rights Reserved -# -# SPDX-License-Identifier: Apache-2.0 -# - -source scripts/setenv.sh - -peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ../test-network/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n bigdatacc -c '{"Args":["get","'$1'"]}' diff --git a/high-throughput/scripts/get-traditional.sh b/high-throughput/scripts/get-traditional.sh deleted file mode 100755 index fa5c1b68..00000000 --- a/high-throughput/scripts/get-traditional.sh +++ /dev/null @@ -1,9 +0,0 @@ -# -# Copyright IBM Corp All Rights Reserved -# -# SPDX-License-Identifier: Apache-2.0 -# - -source scripts/setenv.sh - -peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ../test-network/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n bigdatacc -c '{"Args":["getstandard","'$1'"]}' diff --git a/high-throughput/scripts/many-updates-traditional.sh b/high-throughput/scripts/many-updates-traditional.sh deleted file mode 100755 index dd67792f..00000000 --- a/high-throughput/scripts/many-updates-traditional.sh +++ /dev/null @@ -1,12 +0,0 @@ -# -# Copyright IBM Corp All Rights Reserved -# -# SPDX-License-Identifier: Apache-2.0 -# - -source scripts/setenv.sh - -for (( i = 0; i < 1000; ++i )) -do - peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ../test-network/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n bigdatacc -c '{"Args":["putstandard","'$1'","'$i'"]}' -done diff --git a/high-throughput/scripts/many-updates.sh b/high-throughput/scripts/many-updates.sh deleted file mode 100755 index 7d05272f..00000000 --- a/high-throughput/scripts/many-updates.sh +++ /dev/null @@ -1,12 +0,0 @@ -# -# Copyright IBM Corp All Rights Reserved -# -# SPDX-License-Identifier: Apache-2.0 -# - -source scripts/setenv.sh - -for (( i = 0; i < 1000; ++i )) -do - peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ../test-network/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n bigdatacc -c '{"Args":["update","'$1'","'$2'","'$3'"]}' -done diff --git a/high-throughput/scripts/prune-invoke.sh b/high-throughput/scripts/prune-invoke.sh deleted file mode 100755 index 832843a3..00000000 --- a/high-throughput/scripts/prune-invoke.sh +++ /dev/null @@ -1,9 +0,0 @@ -# -# Copyright IBM Corp All Rights Reserved -# -# SPDX-License-Identifier: Apache-2.0 -# - -source scripts/setenv.sh - -peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ../test-network/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n bigdatacc -c '{"Args":["prune","'$1'"]}' diff --git a/high-throughput/scripts/setenv.sh b/high-throughput/scripts/setenv.sh deleted file mode 100644 index c924f4fa..00000000 --- a/high-throughput/scripts/setenv.sh +++ /dev/null @@ -1,8 +0,0 @@ - -export PATH=${PWD}/../bin:${PWD}:$PATH -export FABRIC_CFG_PATH=$PWD/../config/ -export CORE_PEER_TLS_ENABLED=true -export CORE_PEER_MSPCONFIGPATH=../test-network/organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp -export CORE_PEER_ADDRESS=localhost:7051 -export CORE_PEER_LOCALMSPID="Org1MSP" -export CORE_PEER_TLS_ROOTCERT_FILE=../test-network/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt diff --git a/high-throughput/scripts/update-invoke.sh b/high-throughput/scripts/update-invoke.sh deleted file mode 100755 index 86351a68..00000000 --- a/high-throughput/scripts/update-invoke.sh +++ /dev/null @@ -1,9 +0,0 @@ -# -# Copyright IBM Corp All Rights Reserved -# -# SPDX-License-Identifier: Apache-2.0 -# - -source scripts/setenv.sh - -peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ../test-network/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n bigdatacc -c '{"Args":["update","'$1'","'$2'","'$3'"]}' diff --git a/high-throughput/startFabric.sh b/high-throughput/startFabric.sh index 20bec562..50fe2324 100755 --- a/high-throughput/startFabric.sh +++ b/high-throughput/startFabric.sh @@ -19,7 +19,7 @@ pushd ../test-network echo "Bring up test network" ./network.sh up createChannel -ca -./network.sh deployCC -ccn bigdatacc -ccp ../high-throughput/chaincode/ -ccep "OR('Org1MSP.peer','Org2MSP.peer')" -cci Init +./network.sh deployCC -ccn bigdatacc -ccp ../high-throughput/chaincode-go/ -ccep "OR('Org1MSP.peer','Org2MSP.peer')" -cci Init popd cat < Date: Wed, 4 Nov 2020 09:13:08 -0500 Subject: [PATCH 28/38] Add Asset-Transfer-Events (#325) Add new chaincode and javascript application to demostrate the use of chaincode events and block events with private data. Signed-off-by: Bret Harrison --- .../application-javascript/.eslintignore | 5 + .../application-javascript/.eslintrc.js | 36 ++ .../application-javascript/.gitignore | 14 + .../application-javascript/app.js | 545 ++++++++++++++++++ .../application-javascript/package.json | 16 + .../chaincode-javascript/.eslintignore | 5 + .../chaincode-javascript/.eslintrc.js | 39 ++ .../chaincode-javascript/.gitignore | 15 + .../chaincode-javascript/index.js | 12 + .../lib/assetTransferEvents.js | 128 ++++ .../chaincode-javascript/package.json | 49 ++ .../test/assetTransferEvents.test.js | 224 +++++++ ci/azure-pipelines.yml | 15 + ci/scripts/run-test-network-events.sh | 36 ++ test-network/scripts/deployCC.sh | 5 +- 15 files changed, 1143 insertions(+), 1 deletion(-) create mode 100644 asset-transfer-events/application-javascript/.eslintignore create mode 100644 asset-transfer-events/application-javascript/.eslintrc.js create mode 100644 asset-transfer-events/application-javascript/.gitignore create mode 100644 asset-transfer-events/application-javascript/app.js create mode 100644 asset-transfer-events/application-javascript/package.json create mode 100644 asset-transfer-events/chaincode-javascript/.eslintignore create mode 100644 asset-transfer-events/chaincode-javascript/.eslintrc.js create mode 100644 asset-transfer-events/chaincode-javascript/.gitignore create mode 100644 asset-transfer-events/chaincode-javascript/index.js create mode 100644 asset-transfer-events/chaincode-javascript/lib/assetTransferEvents.js create mode 100644 asset-transfer-events/chaincode-javascript/package.json create mode 100644 asset-transfer-events/chaincode-javascript/test/assetTransferEvents.test.js create mode 100755 ci/scripts/run-test-network-events.sh diff --git a/asset-transfer-events/application-javascript/.eslintignore b/asset-transfer-events/application-javascript/.eslintignore new file mode 100644 index 00000000..15958470 --- /dev/null +++ b/asset-transfer-events/application-javascript/.eslintignore @@ -0,0 +1,5 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# + +coverage diff --git a/asset-transfer-events/application-javascript/.eslintrc.js b/asset-transfer-events/application-javascript/.eslintrc.js new file mode 100644 index 00000000..8422f8a8 --- /dev/null +++ b/asset-transfer-events/application-javascript/.eslintrc.js @@ -0,0 +1,36 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ +'use strict'; + +module.exports = { + env: { + node: true, + mocha: true + }, + parserOptions: { + ecmaVersion: 8, + sourceType: 'script' + }, + extends: 'eslint:recommended', + rules: { + indent: ['error', 'tab'], + 'linebreak-style': ['error', 'unix'], + quotes: ['error', 'single'], + semi: ['error', 'always'], + 'no-unused-vars': ['error', { args: 'none' }], + 'no-console': 'off', + curly: 'error', + eqeqeq: 'error', + 'no-throw-literal': 'error', + 'no-var': 'error', + 'dot-notation': 'error', + 'no-trailing-spaces': 'error', + 'no-use-before-define': 'error', + 'no-useless-call': 'error', + 'no-with': 'error', + 'operator-linebreak': 'error', + yoda: 'error', + 'quote-props': ['error', 'as-needed'] + } +}; diff --git a/asset-transfer-events/application-javascript/.gitignore b/asset-transfer-events/application-javascript/.gitignore new file mode 100644 index 00000000..21b287f7 --- /dev/null +++ b/asset-transfer-events/application-javascript/.gitignore @@ -0,0 +1,14 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# + +# Coverage directory used by tools like istanbul +coverage + +# Dependency directories +node_modules/ +jspm_packages/ +package-lock.json + +wallet +!wallet/.gitkeep diff --git a/asset-transfer-events/application-javascript/app.js b/asset-transfer-events/application-javascript/app.js new file mode 100644 index 00000000..6ff641ac --- /dev/null +++ b/asset-transfer-events/application-javascript/app.js @@ -0,0 +1,545 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; + +/** + * Application that shows events when creating and updating an asset + * -- How to register a contract listener for chaincode events + * -- How to get the chaincode event name and value from the chaincode event + * -- How to retrieve the transaction and block information from the chaincode event + * -- How to register a block listener for full block events + * -- How to retrieve the transaction and block information from the full block event + * -- How to register to recieve private data associated with transactions when + * registering a block listener + * -- How to retreive the private data from the full block event + * -- The listener will be notified of an event at anytime. Notice that events will + * be posted by the listener after the application activity causing the ledger change + * and during other application activity unrelated to the event + * -- How to connect to a Gateway that will not use events when submitting transactions. + * This may be useful when the application does not want to wait for the peer to commit + * blocks and notify the application. + * + * To see the SDK workings, try setting the logging to be displayed on the console + * before executing this application. + * export HFC_LOGGING='{"debug":"console"}' + * See the following on how the SDK is working with the Peer's Event Services + * https://hyperledger-fabric.readthedocs.io/en/latest/peer_event_services.html + * + * See the following for more details on using the Node SDK + * https://hyperledger.github.io/fabric-sdk-node/release-2.2/module-fabric-network.html + */ + +// pre-requisites: +// - fabric-sample two organization test-network setup with two peers, ordering service, +// and 2 certificate authorities +// ===> from directory test-network +// ./network.sh up createChannel -ca +// +// - Use the asset-transfer-events/chaincode-javascript chaincode deployed on +// the channel "mychannel". 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 test-network +// ./network.sh deployCC -ccn events -ccl javascript -ccep "OR('Org1MSP.peer','Org2MSP.peer')" +// +// - Be sure that node.js is installed +// ===> from directory asset-transfer-sbe/application-javascript +// node -v +// - npm installed code dependencies +// ===> from directory asset-transfer-sbe/application-javascript +// npm install +// - to run this test application +// ===> from directory asset-transfer-sbe/application-javascript +// node app.js + +// NOTE: If you see an error like these: +/* + + Error in setup: Error: DiscoveryService: mychannel error: access denied + + OR + + Failed to register user : Error: fabric-ca request register failed with errors [[ { code: 20, message: 'Authentication failure' } ]] + + */ +// Delete the /fabric-samples/asset-transfer-sbe/application-javascript/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. +// + +// use this to set logging, must be set before the require('fabric-network'); +process.env.HFC_LOGGING = '{"debug": "./debug.log"}'; + +const { Gateway, Wallets } = require('fabric-network'); +const EventStrategies = require('fabric-network/lib/impl/event/defaulteventhandlerstrategies'); +const FabricCAServices = require('fabric-ca-client'); +const path = require('path'); +const { buildCAClient, registerAndEnrollUser, enrollAdmin } = require('../../test-application/javascript/CAUtil.js'); +const { buildCCPOrg1, buildWallet } = require('../../test-application/javascript/AppUtil.js'); + +const channelName = 'mychannel'; +const chaincodeName = 'events'; + +const org1 = 'Org1MSP'; +const Org1UserId = 'appUser1'; + +const RED = '\x1b[31m\n'; +const GREEN = '\x1b[32m\n'; +const BLUE = '\x1b[34m'; +const RESET = '\x1b[0m'; + +/** + * Perform a sleep -- asynchronous wait + * @param ms the time in milliseconds to sleep for + */ +function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +async function initGatewayForOrg1(useCommitEvents) { + console.log(`${GREEN}--> Fabric client user & Gateway init: Using Org1 identity to Org1 Peer${RESET}`); + // 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, org1); + // 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, org1, Org1UserId, 'org1.department1'); + + try { + // Create a new gateway for connecting to Org's peer node. + const gatewayOrg1 = new Gateway(); + + if (useCommitEvents) { + await gatewayOrg1.connect(ccpOrg1, { + wallet: walletOrg1, + identity: Org1UserId, + discovery: { enabled: true, asLocalhost: true } + }); + } else { + await gatewayOrg1.connect(ccpOrg1, { + wallet: walletOrg1, + identity: Org1UserId, + discovery: { enabled: true, asLocalhost: true }, + eventHandlerOptions: EventStrategies.NONE + }); + } + + + return gatewayOrg1; + } catch (error) { + console.error(`Error in connecting to gateway for Org1: ${error}`); + process.exit(1); + } +} + +function checkAsset(org, resultBuffer, color, size, owner, appraisedValue, price) { + console.log(`${GREEN}<-- Query results from ${org}${RESET}`); + + let asset; + if (resultBuffer) { + asset = JSON.parse(resultBuffer.toString('utf8')); + } else { + console.log(`${RED}*** Failed to read asset${RESET}`); + } + console.log(`*** verify asset ${asset.ID}`); + + if (asset) { + if (asset.Color === color) { + console.log(`*** asset ${asset.ID} has color ${asset.Color}`); + } else { + console.log(`${RED}*** asset ${asset.ID} has color of ${asset.Color}${RESET}`); + } + if (asset.Size === size) { + console.log(`*** asset ${asset.ID} has size ${asset.Size}`); + } else { + console.log(`${RED}*** Failed size check from ${org} - asset ${asset.ID} has size of ${asset.Size}${RESET}`); + } + if (asset.Owner === owner) { + console.log(`*** asset ${asset.ID} owned by ${asset.Owner}`); + } else { + console.log(`${RED}*** Failed owner check from ${org} - asset ${asset.ID} owned by ${asset.Owner}${RESET}`); + } + if (asset.AppraisedValue === appraisedValue) { + console.log(`*** asset ${asset.ID} has appraised value ${asset.AppraisedValue}`); + } else { + console.log(`${RED}*** Failed appraised value check from ${org} - asset ${asset.ID} has appraised value of ${asset.AppraisedValue}${RESET}`); + } + if (price) { + if (asset.asset_properties && asset.asset_properties.Price === price) { + console.log(`*** asset ${asset.ID} has price ${asset.asset_properties.Price}`); + } else { + console.log(`${RED}*** Failed price check from ${org} - asset ${asset.ID} has price of ${asset.asset_properties.Price}${RESET}`); + } + } + } +} + +function showTransactionData(transactionData) { + const creator = transactionData.actions[0].header.creator; + console.log(` - submitted by: ${creator.mspid}-${creator.id_bytes.toString('hex')}`); + for (const endorsement of transactionData.actions[0].payload.action.endorsements) { + console.log(` - endorsed by: ${endorsement.endorser.mspid}-${endorsement.endorser.id_bytes.toString('hex')}`); + } + const chaincode = transactionData.actions[0].payload.chaincode_proposal_payload.input.chaincode_spec; + console.log(` - chaincode:${chaincode.chaincode_id.name}`); + console.log(` - function:${chaincode.input.args[0].toString()}`); + for (let x = 1; x < chaincode.input.args.length; x++) { + console.log(` - arg:${chaincode.input.args[x].toString()}`); + } +} + +async function main() { + console.log(`${BLUE} **** START ****${RESET}`); + try { + let randomNumber = Math.floor(Math.random() * 1000) + 1; + // use a random key so that we can run multiple times + let assetKey = `item-${randomNumber}`; + + /** ******* Fabric client init: Using Org1 identity to Org1 Peer ******* */ + const gateway1Org1 = await initGatewayForOrg1(true); // transaction handling uses commit events + const gateway2Org1 = await initGatewayForOrg1(); + + try { + // + // - - - - - - C H A I N C O D E E V E N T S + // + console.log(`${BLUE} **** CHAINCODE EVENTS ****${RESET}`); + let transaction; + let listener; + const network1Org1 = await gateway1Org1.getNetwork(channelName); + const contract1Org1 = network1Org1.getContract(chaincodeName); + + try { + // first create a listener to be notified of chaincode code events + // coming from the chaincode ID "events" + listener = async (event) => { + // The payload of the chaincode event is the value place there by the + // chaincode. Notice it is a byte data and the application will have + // to know how to deserialize. + // In this case we know that the chaincode will always place the asset + // being worked with as the payload for all events produced. + const asset = JSON.parse(event.payload.toString()); + console.log(`${GREEN}<-- Contract Event Received: ${event.eventName} - ${JSON.stringify(asset)}${RESET}`); + // show the information available with the event + console.log(`*** Event: ${event.eventName}:${asset.ID}`); + // notice how we have access to the transaction information that produced this chaincode event + const eventTransaction = event.getTransactionEvent(); + console.log(`*** transaction: ${eventTransaction.transactionId} status:${eventTransaction.status}`); + showTransactionData(eventTransaction.transactionData); + // notice how we have access to the full block that contains this transaction + const eventBlock = eventTransaction.getBlockEvent(); + console.log(`*** block: ${eventBlock.blockNumber.toString()}`); + }; + // now start the client side event service and register the listener + console.log(`${GREEN}--> Start contract event stream to peer in Org1${RESET}`); + await contract1Org1.addContractListener(listener); + } catch (eventError) { + console.log(`${RED}<-- Failed: Setup contract events - ${eventError}${RESET}`); + } + + try { + // C R E A T E + console.log(`${GREEN}--> Submit Transaction: CreateAsset, ${assetKey} owned by Sam${RESET}`); + transaction = contract1Org1.createTransaction('CreateAsset'); + await transaction.submit(assetKey, 'blue', '10', 'Sam', '100'); + console.log(`${GREEN}<-- Submit CreateAsset Result: committed, asset ${assetKey}${RESET}`); + } catch (createError) { + console.log(`${RED}<-- Submit Failed: CreateAsset - ${createError}${RESET}`); + } + try { + // R E A D + console.log(`${GREEN}--> Evaluate: ReadAsset, - ${assetKey} should be owned by Sam${RESET}`); + const resultBuffer = await contract1Org1.evaluateTransaction('ReadAsset', assetKey); + checkAsset(org1, resultBuffer, 'blue', '10', 'Sam', '100'); + } catch (readError) { + console.log(`${RED}<-- Failed: ReadAsset - ${readError}${RESET}`); + } + + try { + // U P D A T E + console.log(`${GREEN}--> Submit Transaction: UpdateAsset ${assetKey} update appraised value to 200`); + transaction = contract1Org1.createTransaction('UpdateAsset'); + await transaction.submit(assetKey, 'blue', '10', 'Sam', '200'); + console.log(`${GREEN}<-- Submit UpdateAsset Result: committed, asset ${assetKey}${RESET}`); + } catch (updateError) { + console.log(`${RED}<-- Failed: UpdateAsset - ${updateError}${RESET}`); + } + try { + // R E A D + console.log(`${GREEN}--> Evaluate: ReadAsset, - ${assetKey} should now have appraised value of 200${RESET}`); + const resultBuffer = await contract1Org1.evaluateTransaction('ReadAsset', assetKey); + checkAsset(org1, resultBuffer, 'blue', '10', 'Sam', '200'); + } catch (readError) { + console.log(`${RED}<-- Failed: ReadAsset - ${readError}${RESET}`); + } + + try { + // T R A N S F E R + console.log(`${GREEN}--> Submit Transaction: TransferAsset ${assetKey} to Mary`); + transaction = contract1Org1.createTransaction('TransferAsset'); + await transaction.submit(assetKey, 'Mary'); + console.log(`${GREEN}<-- Submit TransferAsset Result: committed, asset ${assetKey}${RESET}`); + } catch (transferError) { + console.log(`${RED}<-- Failed: TransferAsset - ${transferError}${RESET}`); + } + try { + // R E A D + console.log(`${GREEN}--> Evaluate: ReadAsset, - ${assetKey} should now be owned by Mary${RESET}`); + const resultBuffer = await contract1Org1.evaluateTransaction('ReadAsset', assetKey); + checkAsset(org1, resultBuffer, 'blue', '10', 'Mary', '200'); + } catch (readError) { + console.log(`${RED}<-- Failed: ReadAsset - ${readError}${RESET}`); + } + + try { + // D E L E T E + console.log(`${GREEN}--> Submit Transaction: DeleteAsset ${assetKey}`); + transaction = contract1Org1.createTransaction('DeleteAsset'); + await transaction.submit(assetKey); + console.log(`${GREEN}<-- Submit DeleteAsset Result: committed, asset ${assetKey}${RESET}`); + } catch (deleteError) { + console.log(`${RED}<-- Failed: DeleteAsset - ${deleteError}${RESET}`); + if (deleteError.toString().includes('ENDORSEMENT_POLICY_FAILURE')) { + console.log(`${RED}Be sure that chaincode was deployed with the endorsement policy "OR('Org1MSP.peer','Org2MSP.peer')"${RESET}`) + } + } + try { + // R E A D + console.log(`${GREEN}--> Evaluate: ReadAsset, - ${assetKey} should now be deleted${RESET}`); + const resultBuffer = await contract1Org1.evaluateTransaction('ReadAsset', assetKey); + checkAsset(org1, resultBuffer, 'blue', '10', 'Mary', '200'); + console.log(`${RED}<-- Failed: ReadAsset - should not have read this asset${RESET}`); + } catch (readError) { + console.log(`${GREEN}<-- Success: ReadAsset - ${readError}${RESET}`); + } + + // all done with this listener + contract1Org1.removeContractListener(listener); + + // + // - - - - - - B L O C K E V E N T S with P R I V A T E D A T A + // + console.log(`${BLUE} **** BLOCK EVENTS with PRIVATE DATA ****${RESET}`); + const network2Org1 = await gateway2Org1.getNetwork(channelName); + const contract2Org1 = network2Org1.getContract(chaincodeName); + + randomNumber = Math.floor(Math.random() * 1000) + 1; + assetKey = `item-${randomNumber}`; + + let firstBlock = true; // simple indicator to track blocks + + try { + let listener; + + // create a block listener + listener = async (event) => { + if (firstBlock) { + console.log(`${GREEN}<-- Block Event Received - block number: ${event.blockNumber.toString()}` + + '\n### Note:' + + '\n This block event represents the current top block of the ledger.' + + `\n All block events after this one are events that represent new blocks added to the ledger${RESET}`); + firstBlock = false; + } else { + console.log(`${GREEN}<-- Block Event Received - block number: ${event.blockNumber.toString()}${RESET}`); + } + const transEvents = event.getTransactionEvents(); + for (const transEvent of transEvents) { + console.log(`*** transaction event: ${transEvent.transactionId}`); + if (transEvent.privateData) { + for (const namespace of transEvent.privateData.ns_pvt_rwset) { + console.log(` - private data: ${namespace.namespace}`); + for (const collection of namespace.collection_pvt_rwset) { + console.log(` - collection: ${collection.collection_name}`); + if (collection.rwset.reads) { + for (const read of collection.rwset.reads) { + console.log(` - read set - ${BLUE}key:${RESET} ${read.key} ${BLUE}value:${read.value.toString()}`); + } + } + if (collection.rwset.writes) { + for (const write of collection.rwset.writes) { + console.log(` - write set - ${BLUE}key:${RESET}${write.key} ${BLUE}is_delete:${RESET}${write.is_delete} ${BLUE}value:${RESET}${write.value.toString()}`); + } + } + } + } + } + if (transEvent.transactionData) { + showTransactionData(transEvent.transactionData); + } + } + }; + // now start the client side event service and register the listener + console.log(`${GREEN}--> Start private data block event stream to peer in Org1${RESET}`); + await network2Org1.addBlockListener(listener, {type: 'private'}); + } catch (eventError) { + console.log(`${RED}<-- Failed: Setup block events - ${eventError}${RESET}`); + } + + try { + // C R E A T E + console.log(`${GREEN}--> Submit Transaction: CreateAsset, ${assetKey} owned by Sam${RESET}`); + transaction = contract2Org1.createTransaction('CreateAsset'); + + // create the private data with salt and assign to the transaction + const randomNumber = Math.floor(Math.random() * 100) + 1; + const asset_properties = { + object_type: 'asset_properties', + asset_id: assetKey, + Price: '90', + salt: Buffer.from(randomNumber.toString()).toString('hex') + }; + const asset_properties_string = JSON.stringify(asset_properties); + transaction.setTransient({ + asset_properties: Buffer.from(asset_properties_string) + }); + // With the addition of private data to the transaction + // We must only send this to the organization that will be + // saving the private data or we will get an endorsement policy failure + transaction.setEndorsingOrganizations(org1); + // endorse and commit - private data (transient data) will be + // saved to the implicit collection on the peer + await transaction.submit(assetKey, 'blue', '10', 'Sam', '100'); + console.log(`${GREEN}<-- Submit CreateAsset Result: committed, asset ${assetKey}${RESET}`); + } catch (createError) { + console.log(`${RED}<-- Failed: CreateAsset - ${createError}${RESET}`); + } + await sleep(5000); // need to wait for event to be committed + try { + // R E A D + console.log(`${GREEN}--> Evaluate: ReadAsset, - ${assetKey} should be owned by Sam${RESET}`); + const resultBuffer = await contract2Org1.evaluateTransaction('ReadAsset', assetKey); + checkAsset(org1, resultBuffer, 'blue', '10', 'Sam', '100', '90'); + } catch (readError) { + console.log(`${RED}<-- Failed: ReadAsset - ${readError}${RESET}`); + } + + try { + // U P D A T E + console.log(`${GREEN}--> Submit Transaction: UpdateAsset ${assetKey} update appraised value to 200`); + transaction = contract2Org1.createTransaction('UpdateAsset'); + + // update the private data with new salt and assign to the transaction + const randomNumber = Math.floor(Math.random() * 100) + 1; + const asset_properties = { + object_type: 'asset_properties', + asset_id: assetKey, + Price: '90', + salt: Buffer.from(randomNumber.toString()).toString('hex') + }; + const asset_properties_string = JSON.stringify(asset_properties); + transaction.setTransient({ + asset_properties: Buffer.from(asset_properties_string) + }); + transaction.setEndorsingOrganizations(org1); + + await transaction.submit(assetKey, 'blue', '10', 'Sam', '200'); + console.log(`${GREEN}<-- Submit UpdateAsset Result: committed, asset ${assetKey}${RESET}`); + } catch (updateError) { + console.log(`${RED}<-- Failed: UpdateAsset - ${updateError}${RESET}`); + } + await sleep(5000); // need to wait for event to be committed + try { + // R E A D + console.log(`${GREEN}--> Evaluate: ReadAsset, - ${assetKey} should now have appraised value of 200${RESET}`); + const resultBuffer = await contract2Org1.evaluateTransaction('ReadAsset', assetKey); + checkAsset(org1, resultBuffer, 'blue', '10', 'Sam', '200', '90'); + } catch (readError) { + console.log(`${RED}<-- Failed: ReadAsset - ${readError}${RESET}`); + } + + try { + // T R A N S F E R + console.log(`${GREEN}--> Submit Transaction: TransferAsset ${assetKey} to Mary`); + transaction = contract2Org1.createTransaction('TransferAsset'); + + // update the private data with new salt and assign to the transaction + const randomNumber = Math.floor(Math.random() * 100) + 1; + const asset_properties = { + object_type: 'asset_properties', + asset_id: assetKey, + Price: '180', + salt: Buffer.from(randomNumber.toString()).toString('hex') + }; + const asset_properties_string = JSON.stringify(asset_properties); + transaction.setTransient({ + asset_properties: Buffer.from(asset_properties_string) + }); + transaction.setEndorsingOrganizations(org1); + + await transaction.submit(assetKey, 'Mary'); + console.log(`${GREEN}<-- Submit TransferAsset Result: committed, asset ${assetKey}${RESET}`); + } catch (transferError) { + console.log(`${RED}<-- Failed: TransferAsset - ${transferError}${RESET}`); + } + await sleep(5000); // need to wait for event to be committed + try { + // R E A D + console.log(`${GREEN}--> Evaluate: ReadAsset, - ${assetKey} should now be owned by Mary${RESET}`); + const resultBuffer = await contract2Org1.evaluateTransaction('ReadAsset', assetKey); + checkAsset(org1, resultBuffer, 'blue', '10', 'Mary', '200', '180'); + } catch (readError) { + console.log(`${RED}<-- Failed: ReadAsset - ${readError}${RESET}`); + } + + try { + // D E L E T E + console.log(`${GREEN}--> Submit Transaction: DeleteAsset ${assetKey}`); + transaction = contract2Org1.createTransaction('DeleteAsset'); + await transaction.submit(assetKey); + console.log(`${GREEN}<-- Submit DeleteAsset Result: committed, asset ${assetKey}${RESET}`); + } catch (deleteError) { + console.log(`${RED}<-- Failed: DeleteAsset - ${deleteError}${RESET}`); + } + await sleep(5000); // need to wait for event to be committed + try { + // R E A D + console.log(`${GREEN}--> Evaluate: ReadAsset, - ${assetKey} should now be deleted${RESET}`); + const resultBuffer = await contract2Org1.evaluateTransaction('ReadAsset', assetKey); + checkAsset(org1, resultBuffer, 'blue', '10', 'Mary', '200'); + console.log(`${RED}<-- Failed: ReadAsset - should not have read this asset${RESET}`); + } catch (readError) { + console.log(`${GREEN}<-- Success: ReadAsset - ${readError}${RESET}`); + } + + // all done with this listener + network2Org1.removeBlockListener(listener); + + } catch (runError) { + console.error(`Error in transaction: ${runError}`); + if (runError.stack) { + console.error(runError.stack); + } + } + } catch (error) { + console.error(`Error in setup: ${error}`); + if (error.stack) { + console.error(error.stack); + } + process.exit(1); + } + + await sleep(5000); + console.log(`${BLUE} **** END ****${RESET}`); + process.exit(0); +} +main(); diff --git a/asset-transfer-events/application-javascript/package.json b/asset-transfer-events/application-javascript/package.json new file mode 100644 index 00000000..2767aede --- /dev/null +++ b/asset-transfer-events/application-javascript/package.json @@ -0,0 +1,16 @@ +{ + "name": "asset-transfer-events", + "version": "1.0.0", + "description": "Javascript application that uses chaincode events and block events with private data", + "engines": { + "node": ">=12", + "npm": ">=5" + }, + "engineStrict": true, + "author": "Hyperledger", + "license": "Apache-2.0", + "dependencies": { + "fabric-ca-client": "^2.2.2", + "fabric-network": "^2.2.2" + } +} diff --git a/asset-transfer-events/chaincode-javascript/.eslintignore b/asset-transfer-events/chaincode-javascript/.eslintignore new file mode 100644 index 00000000..15958470 --- /dev/null +++ b/asset-transfer-events/chaincode-javascript/.eslintignore @@ -0,0 +1,5 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# + +coverage diff --git a/asset-transfer-events/chaincode-javascript/.eslintrc.js b/asset-transfer-events/chaincode-javascript/.eslintrc.js new file mode 100644 index 00000000..cb00fa96 --- /dev/null +++ b/asset-transfer-events/chaincode-javascript/.eslintrc.js @@ -0,0 +1,39 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ +'use strict'; + +module.exports = { + env: { + node: true, + mocha: true, + es6: true + }, + parserOptions: { + ecmaVersion: 8, + sourceType: 'script' + }, + extends: 'eslint:recommended', + rules: { + indent: ['error', 'tab'], + 'linebreak-style': ['error', 'unix'], + quotes: ['error', 'single'], + semi: ['error', 'always'], + 'no-unused-vars': ['error', { args: 'none' }], + 'no-console': 'off', + curly: 'error', + eqeqeq: 'error', + 'no-throw-literal': 'error', + strict: 'error', + 'no-var': 'error', + 'dot-notation': 'error', + 'no-trailing-spaces': 'error', + 'no-use-before-define': 'error', + 'no-useless-call': 'error', + 'no-with': 'error', + 'operator-linebreak': 'error', + yoda: 'error', + 'quote-props': ['error', 'as-needed'], + 'no-constant-condition': ['error', { checkLoops: false }] + } +}; diff --git a/asset-transfer-events/chaincode-javascript/.gitignore b/asset-transfer-events/chaincode-javascript/.gitignore new file mode 100644 index 00000000..eeace290 --- /dev/null +++ b/asset-transfer-events/chaincode-javascript/.gitignore @@ -0,0 +1,15 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# + +# Coverage directory used by tools like istanbul +coverage + +# Report cache used by istanbul +.nyc_output + +# Dependency directories +node_modules/ +jspm_packages/ + +package-lock.json diff --git a/asset-transfer-events/chaincode-javascript/index.js b/asset-transfer-events/chaincode-javascript/index.js new file mode 100644 index 00000000..3244cedf --- /dev/null +++ b/asset-transfer-events/chaincode-javascript/index.js @@ -0,0 +1,12 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; + +const assetTransferEvents = require('./lib/assetTransferEvents'); + +module.exports.AssetTransferEvents = assetTransferEvents; +module.exports.contracts = [assetTransferEvents]; diff --git a/asset-transfer-events/chaincode-javascript/lib/assetTransferEvents.js b/asset-transfer-events/chaincode-javascript/lib/assetTransferEvents.js new file mode 100644 index 00000000..27c5acbd --- /dev/null +++ b/asset-transfer-events/chaincode-javascript/lib/assetTransferEvents.js @@ -0,0 +1,128 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; + +const { Contract } = require('fabric-contract-api'); + +async function savePrivateData(ctx, assetKey) { + const clientOrg = ctx.clientIdentity.getMSPID(); + const peerOrg = ctx.stub.getMspID(); + const collection = '_implicit_org_' + peerOrg; + + if (clientOrg === peerOrg) { + const transientMap = ctx.stub.getTransient(); + if (transientMap) { + const properties = transientMap.get('asset_properties'); + if (properties) { + await ctx.stub.putPrivateData(collection, assetKey, properties); + } + } + } +} + +async function removePrivateData(ctx, assetKey) { + const clientOrg = ctx.clientIdentity.getMSPID(); + const peerOrg = ctx.stub.getMspID(); + const collection = '_implicit_org_' + peerOrg; + + if (clientOrg === peerOrg) { + const propertiesBuffer = await ctx.stub.getPrivateData(collection, assetKey); + if (propertiesBuffer && propertiesBuffer.length > 0) { + await ctx.stub.deletePrivateData(collection, assetKey); + } + } +} + +async function addPrivateData(ctx, assetKey, asset) { + const clientOrg = ctx.clientIdentity.getMSPID(); + const peerOrg = ctx.stub.getMspID(); + const collection = '_implicit_org_' + peerOrg; + + if (clientOrg === peerOrg) { + const propertiesBuffer = await ctx.stub.getPrivateData(collection, assetKey); + if (propertiesBuffer && propertiesBuffer.length > 0) { + const properties = JSON.parse(propertiesBuffer.toString()); + asset.asset_properties = properties; + } + } +} + +async function readState(ctx, id) { + const assetBuffer = await ctx.stub.getState(id); // get the asset from chaincode state + if (!assetBuffer || assetBuffer.length === 0) { + throw new Error(`The asset ${id} does not exist`); + } + const assetString = assetBuffer.toString(); + const asset = JSON.parse(assetString); + + return asset; +} + +class AssetTransferEvents extends Contract { + + // CreateAsset issues a new asset to the world state with given details. + async CreateAsset(ctx, id, color, size, owner, appraisedValue) { + const asset = { + ID: id, + Color: color, + Size: size, + Owner: owner, + AppraisedValue: appraisedValue, + }; + await savePrivateData(ctx, id); + const assetBuffer = Buffer.from(JSON.stringify(asset)); + + ctx.stub.setEvent('CreateAsset', assetBuffer); + return ctx.stub.putState(id, assetBuffer); + } + + // TransferAsset updates the owner field of an asset with the given id in + // the world state. + async TransferAsset(ctx, id, newOwner) { + const asset = await readState(ctx, id); + asset.Owner = newOwner; + const assetBuffer = Buffer.from(JSON.stringify(asset)); + await savePrivateData(ctx, id); + + ctx.stub.setEvent('TransferAsset', assetBuffer); + return ctx.stub.putState(id, assetBuffer); + } + + // ReadAsset returns the asset stored in the world state with given id. + async ReadAsset(ctx, id) { + const asset = await readState(ctx, id); + await addPrivateData(ctx, asset.ID, asset); + + return JSON.stringify(asset); + } + + // UpdateAsset updates an existing asset in the world state with provided parameters. + async UpdateAsset(ctx, id, color, size, owner, appraisedValue) { + const asset = await readState(ctx, id); + asset.Color = color; + asset.Size = size; + asset.Owner = owner; + asset.AppraisedValue = appraisedValue; + const assetBuffer = Buffer.from(JSON.stringify(asset)); + await savePrivateData(ctx, id); + + ctx.stub.setEvent('UpdateAsset', assetBuffer); + return ctx.stub.putState(id, assetBuffer); + } + + // DeleteAsset deletes an given asset from the world state. + async DeleteAsset(ctx, id) { + const asset = await readState(ctx, id); + const assetBuffer = Buffer.from(JSON.stringify(asset)); + await removePrivateData(ctx, id); + + ctx.stub.setEvent('DeleteAsset', assetBuffer); + return ctx.stub.deleteState(id); + } +} + +module.exports = AssetTransferEvents; diff --git a/asset-transfer-events/chaincode-javascript/package.json b/asset-transfer-events/chaincode-javascript/package.json new file mode 100644 index 00000000..143b35c7 --- /dev/null +++ b/asset-transfer-events/chaincode-javascript/package.json @@ -0,0 +1,49 @@ +{ + "name": "asset-transfer-events", + "version": "1.0.0", + "description": "Asset-Transfer-Events contract implemented in JavaScript", + "main": "index.js", + "engines": { + "node": ">=12", + "npm": ">=5" + }, + "scripts": { + "lint": "eslint .", + "pretest": "npm run lint", + "test": "nyc mocha --recursive", + "start": "fabric-chaincode-node start" + }, + "engineStrict": true, + "author": "Hyperledger", + "license": "Apache-2.0", + "dependencies": { + "fabric-contract-api": "^2.0.0", + "fabric-shim": "^2.0.0" + }, + "devDependencies": { + "chai": "^4.1.2", + "eslint": "^4.19.1", + "mocha": "^8.0.1", + "nyc": "^14.1.1", + "sinon": "^6.0.0", + "sinon-chai": "^3.2.0" + }, + "nyc": { + "exclude": [ + "coverage/**", + "test/**", + "index.js", + ".eslintrc.js" + ], + "reporter": [ + "text-summary", + "html" + ], + "all": true, + "check-coverage": true, + "statements": 100, + "branches": 100, + "functions": 100, + "lines": 100 + } +} diff --git a/asset-transfer-events/chaincode-javascript/test/assetTransferEvents.test.js b/asset-transfer-events/chaincode-javascript/test/assetTransferEvents.test.js new file mode 100644 index 00000000..b243cdb9 --- /dev/null +++ b/asset-transfer-events/chaincode-javascript/test/assetTransferEvents.test.js @@ -0,0 +1,224 @@ +'use strict'; +const sinon = require('sinon'); +const chai = require('chai'); +const sinonChai = require('sinon-chai'); +const expect = chai.expect; + +const { Context } = require('fabric-contract-api'); +const { ChaincodeStub, ClientIdentity } = require('fabric-shim'); + +const AssetTransfer = require('../lib/assetTransferEvents.js'); + +let assert = sinon.assert; +chai.use(sinonChai); + +describe('Asset Transfer Events Tests', () => { + let transactionContext, chaincodeStub, clientIdentity, asset; + let transientMap, asset_properties; + + beforeEach(() => { + transactionContext = new Context(); + + chaincodeStub = sinon.createStubInstance(ChaincodeStub); + chaincodeStub.getMspID.returns('org1'); + transactionContext.setChaincodeStub(chaincodeStub); + + clientIdentity = sinon.createStubInstance(ClientIdentity); + clientIdentity.getMSPID.returns('org1'); + transactionContext.clientIdentity = clientIdentity; + + chaincodeStub.putState.callsFake((key, value) => { + if (!chaincodeStub.states) { + chaincodeStub.states = {}; + } + chaincodeStub.states[key] = value; + }); + + chaincodeStub.getState.callsFake(async (key) => { + let ret; + if (chaincodeStub.states) { + ret = chaincodeStub.states[key]; + } + return Promise.resolve(ret); + }); + + chaincodeStub.deleteState.callsFake(async (key) => { + if (chaincodeStub.states) { + delete chaincodeStub.states[key]; + } + return Promise.resolve(key); + }); + + chaincodeStub.getStateByRange.callsFake(async () => { + function* internalGetStateByRange() { + if (chaincodeStub.states) { + // Shallow copy + const copied = Object.assign({}, chaincodeStub.states); + + for (let key in copied) { + yield {value: copied[key]}; + } + } + } + + return Promise.resolve(internalGetStateByRange()); + }); + + asset = { + ID: 'asset1', + Color: 'blue', + Size: 5, + Owner: 'Tomoko', + AppraisedValue: 300, + }; + const randomNumber = Math.floor(Math.random() * 100) + 1; + asset_properties = { + object_type: 'asset_properties', + asset_id: 'asset1', + Price: '90', + salt: Buffer.from(randomNumber.toString()).toString('hex') + }; + transientMap = { + asset_properties: Buffer.from(JSON.stringify(asset_properties)) + }; + }); + + describe('Test CreateAsset', () => { + it('should return error on CreateAsset', async () => { + chaincodeStub.putState.rejects('failed inserting key'); + + let assetTransfer = new AssetTransfer(); + try { + await assetTransfer.CreateAsset(transactionContext, asset.ID, asset.Color, asset.Size, asset.Owner, asset.AppraisedValue); + assert.fail('CreateAsset should have failed'); + } catch(err) { + expect(err.name).to.equal('failed inserting key'); + } + }); + + it('should return success on CreateAsset', async () => { + let assetTransfer = new AssetTransfer(); + + await assetTransfer.CreateAsset(transactionContext, asset.ID, asset.Color, asset.Size, asset.Owner, asset.AppraisedValue); + + let ret = JSON.parse((await chaincodeStub.getState(asset.ID)).toString()); + expect(ret).to.eql(asset); + }); + it('should return success on CreateAsset with transient data', async () => { + let assetTransfer = new AssetTransfer(); + chaincodeStub.getTransient.returns(transientMap); + await assetTransfer.CreateAsset(transactionContext, asset.ID, asset.Color, asset.Size, asset.Owner, asset.AppraisedValue); + + let ret = JSON.parse((await chaincodeStub.getState(asset.ID)).toString()); + expect(ret).to.eql(asset); + }); + }); + + describe('Test ReadAsset', () => { + it('should return error on ReadAsset', async () => { + let assetTransfer = new AssetTransfer(); + await assetTransfer.CreateAsset(transactionContext, asset.ID, asset.Color, asset.Size, asset.Owner, asset.AppraisedValue); + + try { + await assetTransfer.ReadAsset(transactionContext, 'asset2'); + assert.fail('ReadAsset should have failed'); + } catch (err) { + expect(err.message).to.equal('The asset asset2 does not exist'); + } + }); + + it('should return success on ReadAsset', async () => { + let assetTransfer = new AssetTransfer(); + await assetTransfer.CreateAsset(transactionContext, asset.ID, asset.Color, asset.Size, asset.Owner, asset.AppraisedValue); + const assetString = await assetTransfer.ReadAsset(transactionContext, 'asset1'); + const readAsset = JSON.parse(assetString); + expect(readAsset).to.eql(asset); + }); + + it('should return success on ReadAsset with private data', async () => { + asset.asset_properties = asset_properties; + let assetTransfer = new AssetTransfer(); + await assetTransfer.CreateAsset(transactionContext, asset.ID, asset.Color, asset.Size, asset.Owner, asset.AppraisedValue); + chaincodeStub.getPrivateData.returns(Buffer.from(JSON.stringify(asset_properties))); + const assetString = await assetTransfer.ReadAsset(transactionContext, 'asset1'); + const readAsset = JSON.parse(assetString); + expect(readAsset).to.eql(asset); + }); + }); + + describe('Test UpdateAsset', () => { + it('should return error on UpdateAsset', async () => { + let assetTransfer = new AssetTransfer(); + await assetTransfer.CreateAsset(transactionContext, asset.ID, asset.Color, asset.Size, asset.Owner, asset.AppraisedValue); + + try { + await assetTransfer.UpdateAsset(transactionContext, 'asset2', 'orange', 10, 'Me', 500); + assert.fail('UpdateAsset should have failed'); + } catch (err) { + expect(err.message).to.equal('The asset asset2 does not exist'); + } + }); + + it('should return success on UpdateAsset', async () => { + let assetTransfer = new AssetTransfer(); + await assetTransfer.CreateAsset(transactionContext, asset.ID, asset.Color, asset.Size, asset.Owner, asset.AppraisedValue); + + await assetTransfer.UpdateAsset(transactionContext, 'asset1', 'orange', 10, 'Me', 500); + let ret = JSON.parse(await chaincodeStub.getState(asset.ID)); + let expected = { + ID: 'asset1', + Color: 'orange', + Size: 10, + Owner: 'Me', + AppraisedValue: 500 + }; + expect(ret).to.eql(expected); + }); + }); + + describe('Test DeleteAsset', () => { + it('should return error on DeleteAsset', async () => { + let assetTransfer = new AssetTransfer(); + await assetTransfer.CreateAsset(transactionContext, asset.ID, asset.Color, asset.Size, asset.Owner, asset.AppraisedValue); + + try { + await assetTransfer.DeleteAsset(transactionContext, 'asset2'); + assert.fail('DeleteAsset should have failed'); + } catch (err) { + expect(err.message).to.equal('The asset asset2 does not exist'); + } + }); + + it('should return success on DeleteAsset', async () => { + let assetTransfer = new AssetTransfer(); + await assetTransfer.CreateAsset(transactionContext, asset.ID, asset.Color, asset.Size, asset.Owner, asset.AppraisedValue); + + await assetTransfer.DeleteAsset(transactionContext, asset.ID); + let ret = await chaincodeStub.getState(asset.ID); + expect(ret).to.equal(undefined); + }); + }); + + describe('Test TransferAsset', () => { + it('should return error on TransferAsset', async () => { + let assetTransfer = new AssetTransfer(); + await assetTransfer.CreateAsset(transactionContext, asset.ID, asset.Color, asset.Size, asset.Owner, asset.AppraisedValue); + + try { + await assetTransfer.TransferAsset(transactionContext, 'asset2', 'Me'); + assert.fail('DeleteAsset should have failed'); + } catch (err) { + expect(err.message).to.equal('The asset asset2 does not exist'); + } + }); + + it('should return success on TransferAsset', async () => { + let assetTransfer = new AssetTransfer(); + await assetTransfer.CreateAsset(transactionContext, asset.ID, asset.Color, asset.Size, asset.Owner, asset.AppraisedValue); + + await assetTransfer.TransferAsset(transactionContext, asset.ID, 'Me'); + let ret = JSON.parse((await chaincodeStub.getState(asset.ID)).toString()); + expect(ret).to.eql(Object.assign({}, asset, {Owner: 'Me'})); + }); + }); +}); diff --git a/ci/azure-pipelines.yml b/ci/azure-pipelines.yml index 0314cf39..5d314a8f 100644 --- a/ci/azure-pipelines.yml +++ b/ci/azure-pipelines.yml @@ -238,3 +238,18 @@ jobs: - script: ../ci/scripts/run-test-network-secured.sh workingDirectory: test-network displayName: Run Test Network Secured Chaincode + + - job: TestNetworkEvents + displayName: Test Network + pool: + vmImage: ubuntu-18.04 + strategy: + matrix: + Events-Javascript: + CHAINCODE_NAME: events + CHAINCODE_LANGUAGE: javascript + steps: + - template: templates/install-deps.yml + - script: ../ci/scripts/run-test-network-events.sh + workingDirectory: test-network + displayName: Run Test Network Events Chaincode diff --git a/ci/scripts/run-test-network-events.sh b/ci/scripts/run-test-network-events.sh new file mode 100755 index 00000000..5899c0c4 --- /dev/null +++ b/ci/scripts/run-test-network-events.sh @@ -0,0 +1,36 @@ +set -euo pipefail + +FABRIC_VERSION=${FABRIC_VERSION:-2.2} +CHAINCODE_LANGUAGE=${CHAINCODE_LANGUAGE:-javascript} +CHAINCODE_NAME=${CHAINCODE_NAME:-events} + +function print() { + GREEN='\033[0;32m' + NC='\033[0m' + echo + echo -e "${GREEN}${1}${NC}" +} + +function createNetwork() { + print "Creating network" + ./network.sh up createChannel -ca + print "Deploying ${CHAINCODE_NAME} chaincode" + ./network.sh deployCC -ccn "${CHAINCODE_NAME}" -ccl "${CHAINCODE_LANGUAGE}" -ccep "OR('Org1MSP.peer','Org2MSP.peer')" +} + +function stopNetwork() { + print "Stopping network" + ./network.sh down +} + +# Run Javascript application +createNetwork +print "Initializing Javascript application" +pushd ../asset-transfer-events/application-javascript +npm install +print "Executing app.js" +node app.js +popd +stopNetwork +print "Remove wallet storage" +rm -R ../asset-transfer-events/application-javascript/wallet diff --git a/test-network/scripts/deployCC.sh b/test-network/scripts/deployCC.sh index 2125a794..edea87a6 100755 --- a/test-network/scripts/deployCC.sh +++ b/test-network/scripts/deployCC.sh @@ -42,6 +42,9 @@ if [ "$CC_SRC_PATH" = "NA" ]; then if [ "$CC_NAME" = "basic" ]; then println $'\e[0;32m'asset-transfer-basic$'\e[0m' chaincode CC_SRC_PATH="../asset-transfer-basic" + elif [ "$CC_NAME" = "events" ]; then + println $'\e[0;32m'asset-transfer-events$'\e[0m' chaincode + CC_SRC_PATH="../asset-transfer-events" elif [ "$CC_NAME" = "secured" ]; then println $'\e[0;32m'asset-transfer-secured-agreeement$'\e[0m' chaincode CC_SRC_PATH="../asset-transfer-secured-agreement" @@ -55,7 +58,7 @@ if [ "$CC_SRC_PATH" = "NA" ]; then println $'\e[0;32m'asset-transfer-sbe$'\e[0m' chaincode CC_SRC_PATH="../asset-transfer-sbe" else - fatalln "The chaincode name ${CC_NAME} is not supported by this script. Supported chaincode names are: basic, ledger, private, sbe, secured" + fatalln "The chaincode name ${CC_NAME} is not supported by this script. Supported chaincode names are: basic, events, ledger, private, sbe, secured" fi # now see what language it is written in From 33adb8d164566aaf7fe853254b8dfa6e314dc838 Mon Sep 17 00:00:00 2001 From: Rijul Aggarwal Date: Wed, 4 Nov 2020 10:27:09 -0500 Subject: [PATCH 29/38] asset transfer basic - application typescript (#339) Signed-off-by: Rijul Aggarwal Co-authored-by: Rijul Aggarwal --- README.md | 2 +- .../application-typescript/.gitignore | 15 + .../application-typescript/package-lock.json | 2601 +++++++++++++++++ .../application-typescript/package.json | 50 + .../application-typescript/src/app.ts | 171 ++ .../src/utils/AppUtil.ts | 72 + .../src/utils/CAUtil.ts | 104 + .../application-typescript/tsconfig.json | 19 + .../application-typescript/tslint.json | 23 + ci/scripts/run-test-network-basic.sh | 12 + 10 files changed, 3068 insertions(+), 1 deletion(-) create mode 100644 asset-transfer-basic/application-typescript/.gitignore create mode 100644 asset-transfer-basic/application-typescript/package-lock.json create mode 100644 asset-transfer-basic/application-typescript/package.json create mode 100644 asset-transfer-basic/application-typescript/src/app.ts create mode 100644 asset-transfer-basic/application-typescript/src/utils/AppUtil.ts create mode 100644 asset-transfer-basic/application-typescript/src/utils/CAUtil.ts create mode 100644 asset-transfer-basic/application-typescript/tsconfig.json create mode 100644 asset-transfer-basic/application-typescript/tslint.json diff --git a/README.md b/README.md index 98e29d4a..c761e44f 100644 --- a/README.md +++ b/README.md @@ -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 | diff --git a/asset-transfer-basic/application-typescript/.gitignore b/asset-transfer-basic/application-typescript/.gitignore new file mode 100644 index 00000000..48285d12 --- /dev/null +++ b/asset-transfer-basic/application-typescript/.gitignore @@ -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 + diff --git a/asset-transfer-basic/application-typescript/package-lock.json b/asset-transfer-basic/application-typescript/package-lock.json new file mode 100644 index 00000000..8a71b6bf --- /dev/null +++ b/asset-transfer-basic/application-typescript/package-lock.json @@ -0,0 +1,2601 @@ +{ + "name": "asset-transfer-basic", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@babel/code-frame": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", + "dev": true, + "requires": { + "@babel/highlight": "^7.10.4" + } + }, + "@babel/generator": { + "version": "7.11.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.11.6.tgz", + "integrity": "sha512-DWtQ1PV3r+cLbySoHrwn9RWEgKMBLLma4OBQloPRyDYvc5msJM9kvTLo1YnlJd1P/ZuKbdli3ijr5q3FvAF3uA==", + "dev": true, + "requires": { + "@babel/types": "^7.11.5", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + } + }, + "@babel/helper-function-name": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", + "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", + "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", + "dev": true, + "requires": { + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz", + "integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==", + "dev": true, + "requires": { + "@babel/types": "^7.11.0" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "dev": true + }, + "@babel/highlight": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.11.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.5.tgz", + "integrity": "sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q==", + "dev": true + }, + "@babel/template": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", + "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "@babel/traverse": { + "version": "7.11.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.11.5.tgz", + "integrity": "sha512-EjiPXt+r7LiCZXEfRpSJd+jUMnBd4/9OUv7Nx3+0u9+eimMwJmG0Q98lw4/289JCoxSE8OolDMNZaaF/JZ69WQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/generator": "^7.11.5", + "@babel/helper-function-name": "^7.10.4", + "@babel/helper-split-export-declaration": "^7.11.0", + "@babel/parser": "^7.11.5", + "@babel/types": "^7.11.5", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.19" + } + }, + "@babel/types": { + "version": "7.11.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", + "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + }, + "@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=" + }, + "@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=" + }, + "@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", + "requires": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=" + }, + "@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=" + }, + "@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=" + }, + "@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=" + }, + "@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=" + }, + "@sinonjs/commons": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.1.tgz", + "integrity": "sha512-892K+kWUUi3cl+LlqEWIDrhvLgdL79tECi8JZUyq6IviKy/DNhuzCRlbHUjxK89f4ypPMMaFnFuR9Ie6DoIMsw==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/formatio": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.2.2.tgz", + "integrity": "sha512-B8SEsgd8gArBLMD6zpRw3juQ2FVSsmdd7qlevyDqzS9WTCtvF55/gAL+h6gue8ZvPYcdiPdvueM/qm//9XzyTQ==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1", + "@sinonjs/samsam": "^3.1.0" + } + }, + "@sinonjs/samsam": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.3.3.tgz", + "integrity": "sha512-bKCMKZvWIjYD0BLGnNrxVuw4dkWCYsLqFOUWw8VgKF/+5Y+mE7LfHWPIYoDXowH+3a9LsWDMo0uAP8YDosPvHQ==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.3.0", + "array-from": "^2.1.1", + "lodash": "^4.17.15" + } + }, + "@sinonjs/text-encoding": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", + "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", + "dev": true + }, + "@types/caseless": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz", + "integrity": "sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==" + }, + "@types/chai": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.12.tgz", + "integrity": "sha512-aN5IAC8QNtSUdQzxu7lGBgYAOuU1tmRU4c9dIq5OKGf/SBVjXo+ffM2wEjudAWbgpOhy60nLoAGH1xm8fpCKFQ==", + "dev": true + }, + "@types/long": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", + "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" + }, + "@types/mocha": { + "version": "5.2.7", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.7.tgz", + "integrity": "sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ==", + "dev": true + }, + "@types/node": { + "version": "10.17.35", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.35.tgz", + "integrity": "sha512-gXx7jAWpMddu0f7a+L+txMplp3FnHl53OhQIF9puXKq3hDGY/GjH+MF04oWnV/adPSCrbtHumDCFwzq2VhltWA==" + }, + "@types/request": { + "version": "2.48.5", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.5.tgz", + "integrity": "sha512-/LO7xRVnL3DxJ1WkPGDQrp4VTV1reX9RkC85mJ+Qzykj2Bdw+mG15aAfDahc76HtknjzE16SX/Yddn6MxVbmGQ==", + "requires": { + "@types/caseless": "*", + "@types/node": "*", + "@types/tough-cookie": "*", + "form-data": "^2.5.0" + } + }, + "@types/sinon": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-5.0.7.tgz", + "integrity": "sha512-opwMHufhUwkn/UUDk35LDbKJpA2VBsZT8WLU8NjayvRLGPxQkN+8XmfC2Xl35MAscBE8469koLLBjaI3XLEIww==", + "dev": true + }, + "@types/sinon-chai": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@types/sinon-chai/-/sinon-chai-3.2.4.tgz", + "integrity": "sha512-xq5KOWNg70PRC7dnR2VOxgYQ6paumW+4pTZP+6uTSdhpYsAUEeeT5bw6rRHHQrZ4KyR+M5ojOR+lje6TGSpUxA==", + "dev": true, + "requires": { + "@types/chai": "*", + "@types/sinon": "*" + } + }, + "@types/tough-cookie": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.0.tgz", + "integrity": "sha512-I99sngh224D0M7XgW1s120zxCt3VYQ3IQsuw3P3jbq5GG4yc79+ZjyKznyOGIQrflfylLgcfekeZW/vk0yng6A==" + }, + "ajv": { + "version": "6.12.5", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.5.tgz", + "integrity": "sha512-lRF8RORchjpKG50/WFf8xmg7sgCLFiYNNnqdKflk63whMQcWR5ngGjiSXkL9bjxy6B2npOK2HSMN49jEBMSkag==", + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "append-transform": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-1.0.0.tgz", + "integrity": "sha512-P009oYkeHyU742iSZJzZZywj4QRJdnTWffaKuJQLablCZ1uz6/cW4yaRgcDaoQ+uwOxxnt0gRUcwfsNP2ri0gw==", + "dev": true, + "requires": { + "default-require-extensions": "^2.0.0" + } + }, + "archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", + "dev": true + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "array-from": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz", + "integrity": "sha1-z+nYwmYoudxa7MYqn12PHzUsEZU=", + "dev": true + }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + }, + "aws4": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.10.1.tgz", + "integrity": "sha512-zg7Hz2k5lI8kb7U32998pRRFin7zJlkfezGJjUc2heaD4Pw2wObakCDVzkKztTm/Ln7eiVvYsjqak0Ed4LkMDA==" + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "requires": { + "tweetnacl": "^0.14.3" + }, + "dependencies": { + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" + } + } + }, + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==" + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" + }, + "browser-request": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/browser-request/-/browser-request-0.3.3.tgz", + "integrity": "sha1-ns5bWsqJopkyJC4Yv5M975h2zBc=" + }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true + }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true + }, + "caching-transform": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-3.0.2.tgz", + "integrity": "sha512-Mtgcv3lh3U0zRii/6qVgQODdPA4G3zhG+jtbCWj39RXuUFTMzH0vcdMtaJS1jPowd+It2Pqr6y3NJMQqOqCE2w==", + "dev": true, + "requires": { + "hasha": "^3.0.0", + "make-dir": "^2.0.0", + "package-hash": "^3.0.0", + "write-file-atomic": "^2.4.2" + } + }, + "callsite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", + "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=" + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "chai": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", + "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==", + "dev": true, + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "pathval": "^1.1.0", + "type-detect": "^4.0.5" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + } + } + }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true + }, + "cloudant-follow": { + "version": "0.18.2", + "resolved": "https://registry.npmjs.org/cloudant-follow/-/cloudant-follow-0.18.2.tgz", + "integrity": "sha512-qu/AmKxDqJds+UmT77+0NbM7Yab2K3w0qSeJRzsq5dRWJTEJdWeb+XpG4OpKuTE9RKOa/Awn2gR3TTnvNr3TeA==", + "requires": { + "browser-request": "~0.3.0", + "debug": "^4.0.1", + "request": "^2.88.0" + } + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commander": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", + "dev": true + }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "convert-source-map": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", + "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.1" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + } + } + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "cp-file": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/cp-file/-/cp-file-6.2.0.tgz", + "integrity": "sha512-fmvV4caBnofhPe8kOcitBwSn2f39QLjnAnGq3gO9dfd75mUytzKNZB1hde6QHunW2Rt+OwuBOMc3i1tNElbszA==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "make-dir": "^2.0.0", + "nested-error-stacks": "^2.0.0", + "pify": "^4.0.1", + "safe-buffer": "^5.0.1" + } + }, + "cross-spawn": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-4.0.2.tgz", + "integrity": "sha1-e5JHYhwjrf3ThWAEqCPL45dCTUE=", + "dev": true, + "requires": { + "lru-cache": "^4.0.1", + "which": "^1.2.9" + }, + "dependencies": { + "lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dev": true, + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", + "dev": true + } + } + }, + "cycle": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz", + "integrity": "sha1-IegLK+hYD5i0aPN5QwZisEbDStI=" + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" + }, + "deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "requires": { + "type-detect": "^4.0.0" + } + }, + "default-require-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-2.0.0.tgz", + "integrity": "sha1-9fj7sYp9bVCyH2QfZJ67Uiz+JPc=", + "dev": true, + "requires": { + "strip-bom": "^3.0.0" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "elliptic": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz", + "integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==", + "requires": { + "bn.js": "^4.4.0", + "brorand": "^1.0.1", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.0" + } + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + }, + "dependencies": { + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + } + } + }, + "errs": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/errs/-/errs-0.3.2.tgz", + "integrity": "sha1-eYCZstvTfKK8dJ5TinwTB9C1BJk=" + }, + "es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, + "eyes": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", + "integrity": "sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A=" + }, + "fabric-ca-client": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/fabric-ca-client/-/fabric-ca-client-2.2.2.tgz", + "integrity": "sha512-hC762b6kEjot7Z9mBfzZDkloKAQ6rNJiKPg3F9o56XfF10xeJ1wj0p6ULMKgQesSYMmre1g9TcALVdnH1NqS8w==", + "requires": { + "fabric-common": "2.2.2", + "jsrsasign": "^8.0.20", + "url": "^0.11.0", + "winston": "^2.4.0" + }, + "dependencies": { + "async": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async/-/async-1.0.0.tgz", + "integrity": "sha1-+PwEyjoTeErenhZBr5hXjPvWR6k=" + }, + "colors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", + "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=" + }, + "winston": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/winston/-/winston-2.4.5.tgz", + "integrity": "sha512-TWoamHt5yYvsMarGlGEQE59SbJHqGsZV8/lwC+iCcGeAe0vUaOh+Lv6SYM17ouzC/a/LB1/hz/7sxFBtlu1l4A==", + "requires": { + "async": "~1.0.0", + "colors": "1.0.x", + "cycle": "1.0.x", + "eyes": "0.1.x", + "isstream": "0.1.x", + "stack-trace": "0.0.x" + } + } + } + }, + "fabric-common": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/fabric-common/-/fabric-common-2.2.2.tgz", + "integrity": "sha512-OhIvZTTis7tSRzztCzss3+BM6R274ZIM1yFc6VZHqubW7SSAy5qkGyzROSHQEim/6HM5meGgEKS5aHKrLzkMtw==", + "requires": { + "callsite": "^1.0.0", + "elliptic": "^6.5.2", + "fabric-protos": "2.2.2", + "js-sha3": "^0.7.0", + "jsrsasign": "^8.0.20", + "nconf": "^0.10.0", + "pkcs11js": "^1.0.6", + "promise-settle": "^0.3.0", + "sjcl": "1.0.7", + "winston": "^2.4.0", + "yn": "^3.1.0" + }, + "dependencies": { + "async": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async/-/async-1.0.0.tgz", + "integrity": "sha1-+PwEyjoTeErenhZBr5hXjPvWR6k=" + }, + "colors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", + "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=" + }, + "winston": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/winston/-/winston-2.4.5.tgz", + "integrity": "sha512-TWoamHt5yYvsMarGlGEQE59SbJHqGsZV8/lwC+iCcGeAe0vUaOh+Lv6SYM17ouzC/a/LB1/hz/7sxFBtlu1l4A==", + "requires": { + "async": "~1.0.0", + "colors": "1.0.x", + "cycle": "1.0.x", + "eyes": "0.1.x", + "isstream": "0.1.x", + "stack-trace": "0.0.x" + } + }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==" + } + } + }, + "fabric-network": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/fabric-network/-/fabric-network-2.2.2.tgz", + "integrity": "sha512-zPZ+yrSyHSVzDVS7f9EEMxN9nKOQ7aDg0RX76efevH7PQ7atDogijMnw2EdYnHBcw/t/ATMlMqaIxkKJNBoPjg==", + "requires": { + "fabric-common": "2.2.2", + "fabric-protos": "2.2.2", + "long": "^4.0.0", + "nano": "^8.2.2" + } + }, + "fabric-protos": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/fabric-protos/-/fabric-protos-2.2.2.tgz", + "integrity": "sha512-deXDIC4l9opKyu7zqZMXqiszxSOULKP66HmWZ435dgmQZyeo0rzFVO7dCwBZYdAVmz9pE4Ze8/qEU2GQ3suCJg==", + "requires": { + "@grpc/grpc-js": "1.0.3", + "@grpc/proto-loader": "0.5.4", + "protobufjs": "^6.9.0" + }, + "dependencies": { + "@grpc/grpc-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.0.3.tgz", + "integrity": "sha512-JKV3f5Bv2TZxK6eJSB9EarsZrnLxrvcFNwI9goq0YRXa3S6NNoCSnI3cG3lkXVIJ03Wng1WXe76kc2JQtRe7AQ==", + "requires": { + "semver": "^6.2.0" + } + }, + "@grpc/proto-loader": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.5.4.tgz", + "integrity": "sha512-HTM4QpI9B2XFkPz7pjwMyMgZchJ93TVkL3kWPW8GDMDKYxsMnmf4w2TNMJK7+KNiYHS5cJrCEAFlF+AwtXWVPA==", + "requires": { + "lodash.camelcase": "^4.3.0", + "protobufjs": "^6.8.6" + } + } + } + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "find-cache-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", + "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^2.0.0", + "pkg-dir": "^3.0.0" + } + }, + "foreground-child": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-1.5.6.tgz", + "integrity": "sha1-T9ca0t/elnibmApcCilZN8svXOk=", + "dev": true, + "requires": { + "cross-spawn": "^4", + "signal-exit": "^3.0.0" + } + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "form-data": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", + "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "dev": true + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, + "graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "dev": true + }, + "growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + }, + "har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "requires": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "requires": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "hasha": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-3.0.0.tgz", + "integrity": "sha1-UqMvq4Vp1BymmmH/GiFPjrfIvTk=", + "dev": true, + "requires": { + "is-stream": "^1.0.1" + }, + "dependencies": { + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true + } + } + }, + "he": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", + "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", + "dev": true + }, + "hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", + "requires": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "hosted-git-info": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", + "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", + "dev": true + }, + "html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" + }, + "invert-kv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", + "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=" + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "istanbul-lib-coverage": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", + "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==", + "dev": true + }, + "istanbul-lib-hook": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-2.0.7.tgz", + "integrity": "sha512-vrRztU9VRRFDyC+aklfLoeXyNdTfga2EI3udDGn4cZ6fpSXpHLV9X6CHvfoMCPtggg8zvDDmC4b9xfu0z6/llA==", + "dev": true, + "requires": { + "append-transform": "^1.0.0" + } + }, + "istanbul-lib-instrument": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz", + "integrity": "sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA==", + "dev": true, + "requires": { + "@babel/generator": "^7.4.0", + "@babel/parser": "^7.4.3", + "@babel/template": "^7.4.0", + "@babel/traverse": "^7.4.3", + "@babel/types": "^7.4.0", + "istanbul-lib-coverage": "^2.0.5", + "semver": "^6.0.0" + } + }, + "istanbul-lib-report": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-2.0.8.tgz", + "integrity": "sha512-fHBeG573EIihhAblwgxrSenp0Dby6tJMFR/HvlerBsrCTD5bkUuoNtn3gVh29ZCS824cGGBPn7Sg7cNk+2xUsQ==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^2.0.5", + "make-dir": "^2.1.0", + "supports-color": "^6.1.0" + }, + "dependencies": { + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz", + "integrity": "sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^2.0.5", + "make-dir": "^2.1.0", + "rimraf": "^2.6.3", + "source-map": "^0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "istanbul-reports": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-2.2.7.tgz", + "integrity": "sha512-uu1F/L1o5Y6LzPVSVZXNOoD/KXpJue9aeLRd0sM9uMXfZvzomB0WxVamWb5ue8kA2vVWEmW7EG+A5n3f1kqHKg==", + "dev": true, + "requires": { + "html-escaper": "^2.0.0" + } + }, + "js-sha3": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.7.0.tgz", + "integrity": "sha512-Wpks3yBDm0UcL5qlVhwW9Jr9n9i4FfeWBFOOXP5puDS/SiudJGhw7DPyBqn3487qD4F0lsC0q3zxink37f7zeA==" + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", + "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "jsrsasign": { + "version": "8.0.24", + "resolved": "https://registry.npmjs.org/jsrsasign/-/jsrsasign-8.0.24.tgz", + "integrity": "sha512-u45jAyusqUpyGbFc2IbHoeE4rSkoBWQgLe/w99temHenX+GyCz4nflU5sjK7ajU1ffZTezl6le7u43Yjr/lkQg==" + }, + "just-extend": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.1.1.tgz", + "integrity": "sha512-aWgeGFW67BP3e5181Ep1Fv2v8z//iBJfrvyTnq8wG86vEESwmonn1zPBJ0VfmT9CJq2FIT0VsETtrNFm2a+SHA==", + "dev": true + }, + "lcid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "requires": { + "invert-kv": "^1.0.0" + } + }, + "load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } + } + }, + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", + "dev": true + }, + "lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=" + }, + "lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=", + "dev": true + }, + "lolex": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-4.2.0.tgz", + "integrity": "sha512-gKO5uExCXvSm6zbF562EvM+rd1kQDnB9AZBbiQVzf1ZmdDpxUSvpnAaVOP83N/31mRK8Ml8/VE8DMvsAZQ+7wg==", + "dev": true + }, + "long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + }, + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "merge-source-map": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.1.0.tgz", + "integrity": "sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw==", + "dev": true, + "requires": { + "source-map": "^0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "mime-db": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", + "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==" + }, + "mime-types": { + "version": "2.1.27", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", + "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", + "requires": { + "mime-db": "1.44.0" + } + }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" + }, + "minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "mocha": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", + "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", + "dev": true, + "requires": { + "browser-stdout": "1.3.1", + "commander": "2.15.1", + "debug": "3.1.0", + "diff": "3.5.0", + "escape-string-regexp": "1.0.5", + "glob": "7.1.2", + "growl": "1.10.5", + "he": "1.1.1", + "minimatch": "3.0.4", + "mkdirp": "0.5.1", + "supports-color": "5.4.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "nan": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz", + "integrity": "sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==", + "optional": true + }, + "nano": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/nano/-/nano-8.2.2.tgz", + "integrity": "sha512-1/rAvpd1J0Os0SazgutWQBx2buAq3KwJpmdIylPDqOwy73iQeAhTSCq3uzbGzvcNNW16Vv/BLXkk+DYcdcH+aw==", + "requires": { + "@types/request": "^2.48.4", + "cloudant-follow": "^0.18.2", + "debug": "^4.1.1", + "errs": "^0.3.2", + "request": "^2.88.0" + } + }, + "nconf": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/nconf/-/nconf-0.10.0.tgz", + "integrity": "sha512-fKiXMQrpP7CYWJQzKkPPx9hPgmq+YLDyxcG9N8RpiE9FoCkCbzD0NyW0YhE3xn3Aupe7nnDeIx4PFzYehpHT9Q==", + "requires": { + "async": "^1.4.0", + "ini": "^1.3.0", + "secure-keys": "^1.0.0", + "yargs": "^3.19.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" + }, + "camelcase": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", + "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=" + }, + "cliui": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wrap-ansi": "^2.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1" + } + }, + "y18n": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", + "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=" + }, + "yargs": { + "version": "3.32.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.32.0.tgz", + "integrity": "sha1-AwiOnr+edWtpdRYR0qXvWRSCyZU=", + "requires": { + "camelcase": "^2.0.1", + "cliui": "^3.0.3", + "decamelize": "^1.1.1", + "os-locale": "^1.4.0", + "string-width": "^1.0.1", + "window-size": "^0.1.4", + "y18n": "^3.2.0" + } + } + } + }, + "nested-error-stacks": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/nested-error-stacks/-/nested-error-stacks-2.1.0.tgz", + "integrity": "sha512-AO81vsIO1k1sM4Zrd6Hu7regmJN1NSiAja10gc4bX3F0wd+9rQmcuHQaHVQCYIEC8iFXnE+mavh23GOt7wBgug==", + "dev": true + }, + "nise": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/nise/-/nise-1.5.3.tgz", + "integrity": "sha512-Ymbac/94xeIrMf59REBPOv0thr+CJVFMhrlAkW/gjCIE58BGQdCj0x7KRCb3yz+Ga2Rz3E9XXSvUyyxqqhjQAQ==", + "dev": true, + "requires": { + "@sinonjs/formatio": "^3.2.1", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "lolex": "^5.0.1", + "path-to-regexp": "^1.7.0" + }, + "dependencies": { + "lolex": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-5.1.2.tgz", + "integrity": "sha512-h4hmjAvHTmd+25JSwrtTIuwbKdwg5NzZVRMLn9saij4SZaepCrTCxPr35H/3bjwfMJtN+t3CX8672UIkglz28A==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0" + } + } + } + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + }, + "nyc": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-14.1.1.tgz", + "integrity": "sha512-OI0vm6ZGUnoGZv/tLdZ2esSVzDwUC88SNs+6JoSOMVxA+gKMB8Tk7jBwgemLx4O40lhhvZCVw1C+OYLOBOPXWw==", + "dev": true, + "requires": { + "archy": "^1.0.0", + "caching-transform": "^3.0.2", + "convert-source-map": "^1.6.0", + "cp-file": "^6.2.0", + "find-cache-dir": "^2.1.0", + "find-up": "^3.0.0", + "foreground-child": "^1.5.6", + "glob": "^7.1.3", + "istanbul-lib-coverage": "^2.0.5", + "istanbul-lib-hook": "^2.0.7", + "istanbul-lib-instrument": "^3.3.0", + "istanbul-lib-report": "^2.0.8", + "istanbul-lib-source-maps": "^3.0.6", + "istanbul-reports": "^2.2.4", + "js-yaml": "^3.13.1", + "make-dir": "^2.1.0", + "merge-source-map": "^1.1.0", + "resolve-from": "^4.0.0", + "rimraf": "^2.6.3", + "signal-exit": "^3.0.2", + "spawn-wrap": "^1.4.2", + "test-exclude": "^5.2.3", + "uuid": "^3.3.2", + "yargs": "^13.2.2", + "yargs-parser": "^13.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dev": true, + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + } + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + } + }, + "yargs": { + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "dev": true, + "requires": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.2" + } + }, + "yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "dev": true + }, + "os-locale": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", + "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", + "requires": { + "lcid": "^1.0.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "package-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-3.0.0.tgz", + "integrity": "sha512-lOtmukMDVvtkL84rJHI7dpTYq+0rli8N2wlnqUcBuDWCfVhRUfOmnR9SsoHFMLpACvEV60dX7rd0rFaYDZI+FA==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.15", + "hasha": "^3.0.0", + "lodash.flattendeep": "^4.4.0", + "release-zalgo": "^1.0.0" + } + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "dev": true + }, + "path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dev": true, + "requires": { + "isarray": "0.0.1" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + } + } + }, + "path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "requires": { + "pify": "^3.0.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } + } + }, + "pathval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", + "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", + "dev": true + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + }, + "pkcs11js": { + "version": "1.0.22", + "resolved": "https://registry.npmjs.org/pkcs11js/-/pkcs11js-1.0.22.tgz", + "integrity": "sha512-g0gOCCkKSa8YfoQPZFKk8VnbPvK+y3Gfx4O9NplzG6ntUX+1HSu491l8IUouxx45Jm3oEQXdDMtdTKH9t695aQ==", + "optional": true, + "requires": { + "nan": "^2.14.1" + } + }, + "pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "dev": true, + "requires": { + "find-up": "^3.0.0" + }, + "dependencies": { + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + } + } + }, + "promise-settle": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/promise-settle/-/promise-settle-0.3.0.tgz", + "integrity": "sha1-tO/VcqHrdM95T4KM00naQKCOTpY=" + }, + "protobufjs": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.10.1.tgz", + "integrity": "sha512-pb8kTchL+1Ceg4lFd5XUpK8PdWacbvV5SK2ULH2ebrYtl4GjJmS24m6CKME67jzV53tbJxHlnNOSqQHbTsR9JQ==", + "requires": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/long": "^4.0.1", + "@types/node": "^13.7.0", + "long": "^4.0.0" + }, + "dependencies": { + "@types/node": { + "version": "13.13.21", + "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.21.tgz", + "integrity": "sha512-tlFWakSzBITITJSxHV4hg4KvrhR/7h3xbJdSFbYJBVzKubrASbnnIFuSgolUh7qKGo/ZeJPKUfbZ0WS6Jp14DQ==" + } + } + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", + "dev": true + }, + "psl": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" + }, + "read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "dev": true, + "requires": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + } + }, + "read-pkg-up": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-4.0.0.tgz", + "integrity": "sha512-6etQSH7nJGsK0RbG/2TeDzZFa8shjQ1um+SwQQ5cwKy0dhSXdOncEhb1CPpvQG4h7FyOV6EB6YlV0yJvZQNAkA==", + "dev": true, + "requires": { + "find-up": "^3.0.0", + "read-pkg": "^3.0.0" + }, + "dependencies": { + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + } + } + }, + "release-zalgo": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", + "integrity": "sha1-CXALflB0Mpc5Mw5TXFqQ+2eFFzA=", + "dev": true, + "requires": { + "es6-error": "^4.0.1" + } + }, + "request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "dependencies": { + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + } + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "resolve": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", + "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + }, + "dependencies": { + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "secure-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/secure-keys/-/secure-keys-1.0.0.tgz", + "integrity": "sha1-8MgtmKOxOah3aogIBQuCRDEIf8o=" + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true + }, + "signal-exit": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", + "dev": true + }, + "sinon": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-7.5.0.tgz", + "integrity": "sha512-AoD0oJWerp0/rY9czP/D6hDTTUYGpObhZjMpd7Cl/A6+j0xBE+ayL/ldfggkBXUs0IkvIiM1ljM8+WkOc5k78Q==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.4.0", + "@sinonjs/formatio": "^3.2.1", + "@sinonjs/samsam": "^3.3.3", + "diff": "^3.5.0", + "lolex": "^4.2.0", + "nise": "^1.5.2", + "supports-color": "^5.5.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "sinon-chai": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/sinon-chai/-/sinon-chai-3.5.0.tgz", + "integrity": "sha512-IifbusYiQBpUxxFJkR3wTU68xzBN0+bxCScEaKMjBvAQERg6FnTTc1F17rseLb1tjmkJ23730AXpFI0c47FgAg==", + "dev": true + }, + "sjcl": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/sjcl/-/sjcl-1.0.7.tgz", + "integrity": "sha1-MrNlpQ3Ju6JriLo8nfjqNCF9n0U=" + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + }, + "source-map-support": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "spawn-wrap": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-1.4.3.tgz", + "integrity": "sha512-IgB8md0QW/+tWqcavuFgKYR/qIRvJkRLPJDFaoXtLLUaVcCDK0+HeFTkmQHj3eprcYhc+gOl0aEA1w7qZlYezw==", + "dev": true, + "requires": { + "foreground-child": "^1.5.6", + "mkdirp": "^0.5.0", + "os-homedir": "^1.0.1", + "rimraf": "^2.6.2", + "signal-exit": "^3.0.2", + "which": "^1.3.0" + } + }, + "spdx-correct": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", + "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.6.tgz", + "integrity": "sha512-+orQK83kyMva3WyPf59k1+Y525csj5JejicWut55zeTWANuN17qSiSLUXWtzHeNWORSvT7GLDJ/E/XiIWoXBTw==", + "dev": true + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "dependencies": { + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" + } + } + }, + "stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=" + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, + "supports-color": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "test-exclude": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-5.2.3.tgz", + "integrity": "sha512-M+oxtseCFO3EDtAaGH7iiej3CBkzXqFMbzqYAACdzKui4eZA+pq3tZEwChvOdNfa7xxy8BfbmgJSIr43cC/+2g==", + "dev": true, + "requires": { + "glob": "^7.1.3", + "minimatch": "^3.0.4", + "read-pkg-up": "^4.0.0", + "require-main-filename": "^2.0.0" + }, + "dependencies": { + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } + } + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "dev": true + }, + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + }, + "ts-node": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-7.0.1.tgz", + "integrity": "sha512-BVwVbPJRspzNh2yfslyT1PSbl5uIk03EZlb493RKHN4qej/D06n1cEhjlOJG69oFsE7OT8XjpTUcYf6pKTLMhw==", + "dev": true, + "requires": { + "arrify": "^1.0.0", + "buffer-from": "^1.1.0", + "diff": "^3.1.0", + "make-error": "^1.1.1", + "minimist": "^1.2.0", + "mkdirp": "^0.5.1", + "source-map-support": "^0.5.6", + "yn": "^2.0.0" + }, + "dependencies": { + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "dev": true + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + } + } + }, + "tslib": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", + "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==", + "dev": true + }, + "tslint": { + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.20.1.tgz", + "integrity": "sha512-EcMxhzCFt8k+/UP5r8waCf/lzmeSyVlqxqMEDQE7rWYiQky8KpIBz1JAoYXfROHrPZ1XXd43q8yQnULOLiBRQg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "builtin-modules": "^1.1.1", + "chalk": "^2.3.0", + "commander": "^2.12.1", + "diff": "^4.0.1", + "glob": "^7.1.1", + "js-yaml": "^3.13.1", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "resolve": "^1.3.2", + "semver": "^5.3.0", + "tslib": "^1.8.0", + "tsutils": "^2.29.0" + }, + "dependencies": { + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "tsutils": { + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", + "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, + "typescript": { + "version": "3.9.7", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.7.tgz", + "integrity": "sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==", + "dev": true + }, + "uri-js": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.0.tgz", + "integrity": "sha512-B0yRTzYdUCCn9n+F4+Gh4yIDtMQcaJsmYBDsTSG8g/OejKBodLQ2IHfN3bM7jUsRXndopT7OIXWdYqc1fjmV6g==", + "requires": { + "punycode": "^2.1.0" + } + }, + "url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + }, + "dependencies": { + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" + } + } + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" + }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "window-size": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.4.tgz", + "integrity": "sha1-+OGqHuWlPsW/FR/6CXQqatdpeHY=" + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "write-file-atomic": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz", + "integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.2" + } + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "dev": true + }, + "yn": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yn/-/yn-2.0.0.tgz", + "integrity": "sha1-5a2ryKz0CPY4X8dklWhMiOavaJo=", + "dev": true + } + } +} diff --git a/asset-transfer-basic/application-typescript/package.json b/asset-transfer-basic/application-typescript/package.json new file mode 100644 index 00000000..beac7df9 --- /dev/null +++ b/asset-transfer-basic/application-typescript/package.json @@ -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 + } +} diff --git a/asset-transfer-basic/application-typescript/src/app.ts b/asset-transfer-basic/application-typescript/src/app.ts new file mode 100644 index 00000000..cd73a346 --- /dev/null +++ b/asset-transfer-basic/application-typescript/src/app.ts @@ -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(); diff --git a/asset-transfer-basic/application-typescript/src/utils/AppUtil.ts b/asset-transfer-basic/application-typescript/src/utils/AppUtil.ts new file mode 100644 index 00000000..f284e30f --- /dev/null +++ b/asset-transfer-basic/application-typescript/src/utils/AppUtil.ts @@ -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 => { + // 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 => { + // 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 => { + // 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, +}; diff --git a/asset-transfer-basic/application-typescript/src/utils/CAUtil.ts b/asset-transfer-basic/application-typescript/src/utils/CAUtil.ts new file mode 100644 index 00000000..665dd0e2 --- /dev/null +++ b/asset-transfer-basic/application-typescript/src/utils/CAUtil.ts @@ -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, 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 => { + 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 => { + 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, +}; diff --git a/asset-transfer-basic/application-typescript/tsconfig.json b/asset-transfer-basic/application-typescript/tsconfig.json new file mode 100644 index 00000000..8d357489 --- /dev/null +++ b/asset-transfer-basic/application-typescript/tsconfig.json @@ -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" + ] +} diff --git a/asset-transfer-basic/application-typescript/tslint.json b/asset-transfer-basic/application-typescript/tslint.json new file mode 100644 index 00000000..a52c3ee2 --- /dev/null +++ b/asset-transfer-basic/application-typescript/tslint.json @@ -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": [] +} diff --git a/ci/scripts/run-test-network-basic.sh b/ci/scripts/run-test-network-basic.sh index 321df301..e6e38d66 100755 --- a/ci/scripts/run-test-network-basic.sh +++ b/ci/scripts/run-test-network-basic.sh @@ -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 From e7c74060f356b5f4c970ddbf93d5bdd4d3dac0b5 Mon Sep 17 00:00:00 2001 From: Sijo Cherian Date: Wed, 4 Nov 2020 10:34:33 -0500 Subject: [PATCH 30/38] Adding assertions in private data js app (#365) Adding assertions in js app, for CI pipeline testing Improved readability Signed-off-by: Sijo Cherian Co-authored-by: Sijo Cherian --- .../application-javascript/app.js | 111 +++++++++++++++--- 1 file changed, 94 insertions(+), 17 deletions(-) diff --git a/asset-transfer-private-data/application-javascript/app.js b/asset-transfer-private-data/application-javascript/app.js index 4c0edb26..0c861514 100644 --- a/asset-transfer-private-data/application-javascript/app.js +++ b/asset-transfer-private-data/application-javascript/app.js @@ -22,6 +22,11 @@ const mspOrg1 = 'Org1MSP'; const mspOrg2 = 'Org2MSP'; const Org1UserId = 'appUser1'; const Org2UserId = 'appUser2'; +const userOrg1IdentityString = `x509::CN=${Org1UserId},OU=client+OU=org1+OU=department1::CN=ca.org1.example.com,O=org1.example.com,L=Durham,ST=North Carolina,C=US`; +const userOrg2IdentityString = `x509::CN=${Org2UserId},OU=client+OU=org2+OU=department1::CN=ca.org2.example.com,O=org2.example.com,L=Hursley,ST=Hampshire,C=UK`; + +const RED = '\x1b[31m\n'; +const RESET = '\x1b[0m'; function prettyJSONString(inputString) { if (inputString) { @@ -32,6 +37,67 @@ function prettyJSONString(inputString) { } } +function doFail(msgString) { + console.error(`${RED}\t${msgString}${RESET}`); + process.exit(1); +} + +function verifyAssetData(org, resultBuffer, expectedId, color, size, owner, appraisedValue) { + + let asset; + if (resultBuffer) { + asset = JSON.parse(resultBuffer.toString('utf8')); + } else { + doFail('Failed to read asset'); + } + console.log(`*** verify asset data for: ${expectedId}`); + if (!asset) { + doFail('Received empty asset'); + } + if (expectedId !== asset.assetID) { + doFail(`recieved asset ${asset.assetID} , but expected ${expectedId}`); + } + if (asset.color !== color) { + doFail(`asset ${asset.assetID} has color of ${asset.color}, expected value ${color}`); + } + if (asset.size !== size) { + doFail(`Failed size check - asset ${asset.assetID} has size of ${asset.size}, expected value ${size}`); + } + let assetsOwner = Buffer.from(asset.owner, 'base64').toString(); + if (assetsOwner === owner) { + console.log(`\tasset ${asset.assetID} owner: ${assetsOwner}`); + } else { + doFail(`Failed owner check from ${org} - asset ${asset.assetID} owned by ${assetsOwner}, expected value ${owner}`); + } + if (appraisedValue) { + if (asset.appraisedValue !== appraisedValue) { + doFail(`Failed appraised value check from ${org} - asset ${asset.assetID} has appraised value of ${asset.appraisedValue}, expected value ${appraisedValue}`); + } + } +} + +function verifyAssetPrivateDetails(resultBuffer, expectedId, appraisedValue) { + let assetPD; + if (resultBuffer) { + assetPD = JSON.parse(resultBuffer.toString('utf8')); + } else { + doFail('Failed to read asset private details'); + } + console.log(`*** verify private details: ${expectedId}`); + if (!assetPD) { + doFail('Received empty data'); + } + if (expectedId !== assetPD.assetID) { + doFail(`recieved ${assetPD.assetID} , but expected ${expectedId}`); + } + + if (appraisedValue) { + if (assetPD.appraisedValue !== appraisedValue) { + doFail(`Failed appraised value check - asset ${assetPD.assetID} has appraised value of ${assetPD.appraisedValue}, expected value ${appraisedValue}`); + } + } +} + 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) @@ -115,8 +181,10 @@ async function main() { 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'; + let randomNumber = Math.floor(Math.random() * 1000) + 1; + // use a random key so that we can run multiple times + let assetID1 = `asset${randomNumber}`; + let assetID2 = `asset${randomNumber + 1}`; const assetType = 'ValuableAsset'; let result; let asset1Data = { objectType: assetType, assetID: assetID1, color: 'green', size: 20, appraisedValue: 100 }; @@ -146,12 +214,15 @@ async function main() { 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(`<-- result: ${prettyJSONString(result.toString())}`); + if (!result || result.length === 0) { + doFail('recieved empty query list for GetAssetByRange'); + } 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())); + console.log(`<-- result: ${prettyJSONString(result.toString())}`); + verifyAssetPrivateDetails(result, assetID1, 100); // Attempt Transfer the asset to Org2 , without Org2 adding AgreeToTransfer // // Transaction should return an error: "failed transfer verification ..." @@ -171,9 +242,9 @@ async function main() { 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()); + console.log(`<-- result: ${prettyJSONString(result.toString())}`); + verifyAssetData(mspOrg2, result, assetID1, 'green', 20, userOrg1IdentityString); + // Org2 cannot ReadAssetPrivateDetails from Org1's private collection due to Collection policy // Will fail: await contractOrg2.evaluateTransaction('ReadAssetPrivateDetails', org1PrivateCollectionName, assetID1); @@ -203,7 +274,7 @@ async function main() { // 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())); + 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 @@ -219,19 +290,21 @@ async function main() { //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()); + console.log(`<-- result: ${prettyJSONString(result.toString())}`); + verifyAssetData(mspOrg1, result, assetID1, 'green', 20, userOrg2IdentityString); //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(`<-- result: ${prettyJSONString(result.toString())}`); + if (result && result.length > 0) { + doFail('Expected empty data from ReadAssetPrivateDetails'); + } console.log('\n--> Evaluate Transaction: ReadAsset ' + assetID2); result = await contractOrg1.evaluateTransaction('ReadAsset', assetID2); - console.log(' result: ' + prettyJSONString(result.toString())); + console.log(`<-- result: ${prettyJSONString(result.toString())}`); + verifyAssetData(mspOrg1, result, assetID2, 'blue', 35, userOrg1IdentityString); console.log('\n********* Demo deleting asset **************'); let dataForDelete = { assetID: assetID2 }; @@ -259,13 +332,17 @@ async function main() { console.log('\n--> Evaluate Transaction: ReadAsset ' + assetID2); result = await contractOrg1.evaluateTransaction('ReadAsset', assetID2); - console.log(' result: ' + prettyJSONString(result.toString())); + console.log(`<-- result: ${prettyJSONString(result.toString())}`); + if (result && result.length > 0) { + doFail('Expected empty read, after asset is deleted'); + } 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())); + console.log(`<-- result: ${prettyJSONString(result.toString())}`); + verifyAssetPrivateDetails(result, assetID1, 100); } finally { // Disconnect from the gateway peer when all work for this client identity is complete gatewayOrg1.disconnect(); From 981893ab4d127b717020cab8caf0c6e9592cc1a4 Mon Sep 17 00:00:00 2001 From: Bram Dufour <40569073+braduf@users.noreply.github.com> Date: Wed, 4 Nov 2020 15:43:54 -0500 Subject: [PATCH 31/38] Signed-off-by: Bram Dufour (#366) Added check to avoid double spent in token-utxo chaincode In the transfer function it was possible to pass the same utxo more than once in the array of input utxos without any error. Making it possible to spend the same utxo more than once in the same transaction and like this create more tokens than minted. To fix this I changed the array that was used to store the input utxo states to a map and so utxos were stored in the map by there key, which made it possible to know if a key and so a utxo had already been used. I tested this by setting up the test-network, deploying the token-utxo chaincode with my fix, minting a utxo and trying to spent this utxo more than once in the same transaction, which was not possible anymore. It can only be spent one time anymore with this fix. --- token-utxo/chaincode-go/chaincode/token_contract.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/token-utxo/chaincode-go/chaincode/token_contract.go b/token-utxo/chaincode-go/chaincode/token_contract.go index cc96b309..963e11aa 100644 --- a/token-utxo/chaincode-go/chaincode/token_contract.go +++ b/token-utxo/chaincode-go/chaincode/token_contract.go @@ -70,9 +70,13 @@ func (s *SmartContract) Transfer(ctx contractapi.TransactionContextInterface, ut } // Validate and summarize utxo inputs - var utxoInputs []*UTXO + utxoInputs := make(map[string]*UTXO) var totalInputAmount int for _, utxoInputKey := range utxoInputKeys { + if utxoInputs[utxoInputKey] != nil { + return nil, fmt.Errorf("the same utxo input can not be spend twice") + } + utxoInputCompositeKey, err := ctx.GetStub().CreateCompositeKey("utxo", []string{clientID, utxoInputKey}) if err != nil { return nil, fmt.Errorf("failed to create composite key: %v", err) @@ -97,7 +101,7 @@ func (s *SmartContract) Transfer(ctx contractapi.TransactionContextInterface, ut } totalInputAmount += amount - utxoInputs = append(utxoInputs, utxoInput) + utxoInputs[utxoInputKey] = utxoInput } // Validate and summarize utxo outputs From bf1d9fc667ab1c2bd2a840ddd3f700e5d0db3a1b Mon Sep 17 00:00:00 2001 From: nikhil550 Date: Fri, 6 Nov 2020 07:09:35 -0500 Subject: [PATCH 32/38] Create auction sample (#355) Signed-off-by: NIKHIL E GUPTA Co-authored-by: NIKHIL E GUPTA --- auction/README.md | 423 ++++++++++++++ auction/application-javascript/bid.js | 114 ++++ .../application-javascript/closeAuction.js | 109 ++++ .../application-javascript/createAuction.js | 95 +++ auction/application-javascript/endAuction.js | 109 ++++ auction/application-javascript/enrollAdmin.js | 76 +++ auction/application-javascript/package.json | 16 + .../application-javascript/queryAuction.js | 86 +++ auction/application-javascript/queryBid.js | 87 +++ .../registerEnrollUser.js | 77 +++ auction/application-javascript/revealBid.js | 119 ++++ auction/application-javascript/submitBid.js | 108 ++++ auction/chaincode-go/go.mod | 8 + auction/chaincode-go/go.sum | 139 +++++ .../chaincode-go/smart-contract/auction.go | 539 ++++++++++++++++++ .../smart-contract/auctionQueries.go | 148 +++++ auction/chaincode-go/smart-contract/utils.go | 107 ++++ auction/chaincode-go/smartContract.go | 23 + auction/dutch-auction/README.md | 431 ++++++++++++++ 19 files changed, 2814 insertions(+) create mode 100644 auction/README.md create mode 100644 auction/application-javascript/bid.js create mode 100644 auction/application-javascript/closeAuction.js create mode 100644 auction/application-javascript/createAuction.js create mode 100644 auction/application-javascript/endAuction.js create mode 100644 auction/application-javascript/enrollAdmin.js create mode 100644 auction/application-javascript/package.json create mode 100644 auction/application-javascript/queryAuction.js create mode 100644 auction/application-javascript/queryBid.js create mode 100644 auction/application-javascript/registerEnrollUser.js create mode 100644 auction/application-javascript/revealBid.js create mode 100644 auction/application-javascript/submitBid.js create mode 100644 auction/chaincode-go/go.mod create mode 100644 auction/chaincode-go/go.sum create mode 100644 auction/chaincode-go/smart-contract/auction.go create mode 100644 auction/chaincode-go/smart-contract/auctionQueries.go create mode 100644 auction/chaincode-go/smart-contract/utils.go create mode 100644 auction/chaincode-go/smartContract.go create mode 100644 auction/dutch-auction/README.md diff --git a/auction/README.md b/auction/README.md new file mode 100644 index 00000000..21e74397 --- /dev/null +++ b/auction/README.md @@ -0,0 +1,423 @@ +## Auction sample + +The auction sample uses Hyperledger Fabric to run an auction where bids kept are private from other bidders. Instead of displaying the full bid on the public ledger, buyers can only see hashes of other bids while bidding is underway. This prevents potential buyers from changing their bids in response to bids that have already been submitted. After the bidding period ends, participants can reveal their bid to try to win the auction. The organizations participating in the auction verify that a revealed bid matches the hash on the public ledger. + +A user that wants to sell one or more items can use the smart contract to create an auction. The auction is stored on the channel ledger and can be read by all channel members. The auctions created by the smart contract are run in three steps: +1. Each auction is created with the status **open**. While the auction is open, potential buyers can add new bids to the auction. The full bids of each buyer are stored in the implicit private data collection of their organization. After the bid is created, the bidder can submit the hash of the bid to the auction. A bid is added to the auction in two steps because the transaction that creates the bid only needs be endorsed by a peer of the bidder's organization, while a transaction that updates the auction may need to be endorsed by multiple organizations. When the bid is added to the auction, the bidder's organization is added to the list of organizations that need to endorse any updates to the auction. +2. The auction is **closed** to prevent additional bids from being added to the auction. After the auction is closed, the bidders that submitted bids to the auction can reveal their full bid. Only revealed bids can win the auction. +3. The auction is **ended** to calculate the winners from the set of revealed bids. All organizations participating in the auction calculate the price that clears the auction and the winning set of bids. The seller can end the auction only if all bidding organizations endorse the same winners and price. + + Before endorsing the transaction that ends the auction, each organization queries the implicit private data collection on their peers to check if any organization member has a winning bid that has not yet been revealed. If a winning bid is found, the organization will withhold their endorsement and prevent the auction from being closed. This prevents the seller from ending the auction prematurely, or colluding with buyers to end the auction at an artificially low price. + +The sample uses several Fabric features to make the auction private and secure. Bids are stored in private data collections to prevent bids from being distributed to other peers in the channel. When bidding is closed, the auction smart contract uses the `GetPrivateDataHash()` API to verify that the bid stored in private data is the same bid as the one that is being revealed. State based endorsement is used to add the organization of each bidder to the auction endorsement policy. The smart contract uses the `GetClientIdentity.GetID()` API to ensure that only the potential buyer can read their bid from private state and only the seller can close or end the auction. + +This tutorial uses the auction smart contract in a scenario where one seller wants to sell a valuable painting. Four potential buyers from two different organizations will submit bids to purchase the painting. You can also use the auction smart contract implement a [Dutch auction](https://en.wikipedia.org/wiki/Dutch_auction) for multiple items of the same type. To run an auction that sells multiple items, see [Running a Dutch auction](dutch-auction). + +## Deploy the chaincode + +We will run the auction smart contract using the Fabric test network. Open a command terminal and navigate to the test network directory: +``` +cd fabric-samples/test-network +``` + +You can then run the following command to deploy the test network. +``` +./network.sh up createChannel -ca +``` + +Note that we use the `-ca` flag to deploy the network using certificate authorities. We will use the CA to register and enroll our sellers and buyers. + +Run the following command to deploy the auction smart contract. We will override the default endorsement policy to allow any channel member to create an auction without requiring an endorsement from another organization. +``` +./network.sh deployCC -ccn auction -ccp ../auction/chaincode-go/ -ccep "OR('Org1MSP.peer','Org2MSP.peer')" +``` + +## Install the application dependencies + +We will interact with the auction smart contract through a set of Node.js applications. Change into the `application-javascript` directory: +``` +cd fabric-samples/auction/application-javascript +``` + +From this directory, run the following command to download the application dependencies: +``` +npm install +``` + +## Register and enroll the application identities + +To interact with the network, you will need to enroll the Certificate Authority administrators of Org1 and Org2. You can use the `enrollAdmin.js` program for this task. Run the following command to enroll the Org1 admin: +``` +node enrollAdmin.js org1 +``` +You should see the logs of the admin wallet being created on your local file system. Now run the command to enroll the CA admin of Org2: +``` +node enrollAdmin.js org2 +``` + +We can use the CA admins of both organizations to register and enroll the identities of the seller that will create the auction and the bidders who will try to purchase the painting. + +Run the following command to register and enroll the seller identity that will create the auction. The seller will belong to Org1. +``` +node registerEnrollUser.js org1 seller +``` + +You should see the logs of the seller wallet being created as well. Run the following commands to register and enroll 2 bidders from Org1 and another 2 bidders from Org2: +``` +node registerEnrollUser.js org1 bidder1 +node registerEnrollUser.js org1 bidder2 +node registerEnrollUser.js org2 bidder3 +node registerEnrollUser.js org2 bidder4 +``` + +## Create the auction + +The seller from Org1 would like to create an auction for the painting. Run the following command to use the seller wallet to run the `createAuction.js` application. The program will submit a transaction to the network that creates the auction on the channel ledger. The organization and identity name are passed to the application to use the wallet that was created by the `registerEnrollUser.js` application. The seller needs to provide an ID for the auction, the item to be sold, and the quantity to be sold to create the auction: +``` +node createAuction.js org1 seller auction1 painting 1 +``` + +After the transaction is complete, the `createAuction.js` application will query the auction stored in the public channel ledger: +``` +*** Result: Auction: { + "objectType": "auction", + "item": "painting", + "seller": "eDUwOTo6Q049c2VsbGVyLE9VPWNsaWVudCtPVT1vcmcxK09VPWRlcGFydG1lbnQxOjpDTj1jYS5vcmcxLmV4YW1wbGUuY29tLE89b3JnMS5leGFtcGxlLmNvbSxMPUR1cmhhbSxTVD1Ob3J0aCBDYXJvbGluYSxDPVVT", + "quantity": 1, + "organizations": [ + "Org1MSP" + ], + "privateBids": {}, + "revealedBids": {}, + "winners": [], + "price": 0, + "status": "open" +} +``` +The smart contract uses the `GetClientIdentity().GetID()` API to read identity that creates the auction and defines that identity as the auction `"seller"`. You can see the seller information by decoding the `"seller"` string out of base64 format: + +``` +echo eDUwOTo6Q049c2VsbGVyLE9VPWNsaWVudCtPVT1vcmcxK09VPWRlcGFydG1lbnQxOjpDTj1jYS5vcmcxLmV4YW1wbGUuY29tLE89b3JnMS5leGFtcGxlLmNvbSxMPUR1cmhhbSxTVD1Ob3J0aCBDYXJvbGluYSxDPVVT | base64 --decode +``` + +The result is the name and issuer of the seller's certificate: +``` +x509::CN=org1admin,OU=admin,O=Hyperledger,ST=North Carolina,C=US::CN=ca.org1.example.com,O=org1.example.com,L=Durham,ST=North Carolina,C=USn +``` + +## Bid on the auction + +We can now use the bidder wallets to submit bids to the auction: + +### Bid as bidder1 + +Bidder1 will create a bid to purchase the painting for 800 dollars. +``` +node bid.js org1 bidder1 auction1 1 800 +``` + +The application will query the bid after it is created: +``` +*** Result: Bid: { + "objectType": "bid", + "quantity": 1, + "price": 800, + "org": "Org1MSP", + "buyer": "eDUwOTo6Q049YmlkZGVyMSxPVT1jbGllbnQrT1U9b3JnMStPVT1kZXBhcnRtZW50MTo6Q049Y2Eub3JnMS5leGFtcGxlLmNvbSxPPW9yZzEuZXhhbXBsZS5jb20sTD1EdXJoYW0sU1Q9Tm9ydGggQ2Fyb2xpbmEsQz1VUw==" +} +``` + +The bid is stored in the Org1 implicit data collection. The `"buyer"` parameter is the information from the certificate of the user that created the bid. Only this identity will be able to query the bid from private state or reveal the bid during the auction. + +The `bid.js` application also prints the bidID: +``` +*** Result ***SAVE THIS VALUE*** BidID: 68d3de99032700d92b2d753c79d5aff2ca79378b169efb6a70ca3e0ea43acb1b +``` + +The BidID acts as the unique identifier for the bid. This ID allows you to query the bid using the `queryBid.js` program and add the bid to the auction. Save the bidID returned by the application as an environment variable in your terminal: +``` +export BIDDER1_BID_ID=68d3de99032700d92b2d753c79d5aff2ca79378b169efb6a70ca3e0ea43acb1b +``` +This value will be different for each transaction, so you will need to use the value returned in your terminal. + +Now that the bid has been created, you can submit the bid to the auction. Run the following command to submit the bid that was just created: +``` +node submitBid.js org1 bidder1 auction1 $BIDDER1_BID_ID +``` + +The hash of bid is added to the list of private bids in that have been submitted to `auction1`. Storing the hash on the public auction ledger allows users to prove the accuracy of the bids they reveal once bidding is closed. The application queries the auction to verify that the bid was added: +``` +*** Result: Auction: { + "objectType": "auction", + "item": "painting", + "seller": "eDUwOTo6Q049c2VsbGVyLE9VPWNsaWVudCtPVT1vcmcxK09VPWRlcGFydG1lbnQxOjpDTj1jYS5vcmcxLmV4YW1wbGUuY29tLE89b3JnMS5leGFtcGxlLmNvbSxMPUR1cmhhbSxTVD1Ob3J0aCBDYXJvbGluYSxDPVVT", + "quantity": 1, + "organizations": [ + "Org1MSP" + ], + "privateBids": { + "\u0000bid\u0000auction1\u000003382872f8f9dc94f211385d3d127f155e22de1bee8e112dcb90beb3e78f2722\u0000": { + "org": "Org1MSP", + "hash": "9345d28bb40b7026cc9e14743147356ae1cac99ced8190fe72c9be69fbfc4b71" + } + }, + "revealedBids": {}, + "winners": [], + "price": 0, + "status": "open" +} +``` + +### Bid as bidder2 + +Let's submit another bid. Bidder2 would like to purchase the painting for 500 dollars. +``` +node bid.js org1 bidder2 auction1 1 500 +``` + +Save the Bid ID returned by the application: +``` +export BIDDER2_BID_ID=77dc57876a1bc5cb798863f07eaab6f041608e55c60900b2cbcf0acc7723e8ec +``` + +Submit bidder2's bid to the auction: +``` +node submitBid.js org1 bidder2 auction1 $BIDDER2_BID_ID +``` + +### Bid as bidder3 from Org2 + +Bidder3 will bid 700 dollars for the painting: +``` +node bid.js org2 bidder3 auction1 1 700 +``` + +Save the Bid ID returned by the application: +``` +export BIDDER3_BID_ID=7e2d0c33d0ff1030d855e5fb76f2a4eb30589549b4cb6e581da17d3705cbf77e +``` + +Add bidder3's bid to the auction: +``` +node submitBid.js org2 bidder3 auction1 $BIDDER3_BID_ID +``` + +Because bidder3 belongs to Org2, submitting the bid will add Org2 to the list of participating organizations. You can see the Org2 MSP ID has been added to the list of `"organizations"` in the updated auction returned by the application: +``` +*** Result: Auction: { + "objectType": "auction", + "item": "painting", + "seller": "eDUwOTo6Q049c2VsbGVyLE9VPWNsaWVudCtPVT1vcmcxK09VPWRlcGFydG1lbnQxOjpDTj1jYS5vcmcxLmV4YW1wbGUuY29tLE89b3JnMS5leGFtcGxlLmNvbSxMPUR1cmhhbSxTVD1Ob3J0aCBDYXJvbGluYSxDPVVT", + "quantity": 1, + "organizations": [ + "Org1MSP", + "Org2MSP" + ], + "privateBids": { + "\u0000bid\u0000auction1\u000003382872f8f9dc94f211385d3d127f155e22de1bee8e112dcb90beb3e78f2722\u0000": { + "org": "Org1MSP", + "hash": "9345d28bb40b7026cc9e14743147356ae1cac99ced8190fe72c9be69fbfc4b71" + }, + "\u0000bid\u0000auction1\u0000347e08fbbf766a0f3678c3c6e05b0613026fa4d4619ad80869773938ed893f8d\u0000": { + "org": "Org2MSP", + "hash": "ecaf72ecd6daf27889434491b0e5580c94bd682d8b0283c142b085d51fcf1f69" + }, + "\u0000bid\u0000auction1\u0000e624a5f04fb8878e0d20bf10649f06113771eba6ea4c58f46d17264d1b492a46\u0000": { + "org": "Org1MSP", + "hash": "8434bfe683c4436484176e6c9b21a5d684c0710fce9fd2506af4e05bb0f8a3ea" + } + }, + "revealedBids": {}, + "winners": [], + "price": 0, + "status": "open" +} +``` + +Now that a bid from Org2 has been added to the auction, any updates to the auction need to be endorsed by the Org2 peer. The applications will use the `"organizations"` field to specify which organizations need to endorse submitting a new bid, revealing a bid, or updating the auction status. + +### Bid as bidder4 + +Bidder4 from Org2 would like to purchase the painting for 900 dollars: +``` +node bid.js org2 bidder4 auction1 1 900 +``` + +Save the Bid ID returned by the application: +``` +export BIDDER4_BID_ID=083478f1af2ba391a5b8d7c590cfb790aedf11578d64ebc4e5efe793555ff212 +``` + +Add bidder4's bid to the auction: +``` +node submitBid.js org2 bidder4 auction1 $BIDDER4_BID_ID +``` + +## Close the auction + +Now that all four bidders have joined the auction, the seller would like to close the auction and allow buyers to reveal their bids. The seller identity that created the auction needs to submit the transaction: +``` +node closeAuction.js org1 seller auction1 +``` + +The application will query the auction to allow you to verify that the auction status has changed to closed. As a test, you can try to create and submit a new bid to verify that no new bids can be added to the auction. + +## Reveal bids + +After the auction is closed, bidders can try to win the auction by revealing their bids. The transaction to reveal a bid needs to pass four checks: +1. The auction is closed. +2. The transaction was submitted by the identity that created the bid. +3. The hash of the revealed bid matches the hash of the bid on the channel ledger. This confirms that the bid is the same as the bid that is stored in the private data collection. +4. The hash of the revealed bid matches the hash that was submitted to the auction. This confirms that the bid was not altered after the auction was closed. + +Use the `revealBid.js` application to reveal the bid of Bidder1: +``` +node revealBid.js org1 bidder1 auction1 $BIDDER1_BID_ID +``` + +The full bid details, including the quantity and price, are now visible: +``` +*** Result: Auction: { + "objectType": "auction", + "item": "painting", + "seller": "eDUwOTo6Q049c2VsbGVyLE9VPWNsaWVudCtPVT1vcmcxK09VPWRlcGFydG1lbnQxOjpDTj1jYS5vcmcxLmV4YW1wbGUuY29tLE89b3JnMS5leGFtcGxlLmNvbSxMPUR1cmhhbSxTVD1Ob3J0aCBDYXJvbGluYSxDPVVT", + "quantity": 100, + "organizations": [ + "Org1MSP", + "Org2MSP" + ], + "privateBids": { + "\u0000bid\u0000auction1\u00002fcaee344b361a95701dc03ca4417c6c158845f9e4be3b2555b17c2b691c5ec6\u0000": { + "org": "Org2MSP", + "hash": "d6f661d8b664244ce55065edc2fd95a982221888bb19afdad931b185e187ed4f" + }, + "\u0000bid\u0000auction1\u000083d529d37ea3518c67135fa8b9bf0bff33053bed0e2f38cb6a7ff75882efcf95\u0000": { + "org": "Org2MSP", + "hash": "4446c7eb0e2d64165a916ee996348a18716f4c97e632d58d5a8c20eeec5a9238" + }, + "\u0000bid\u0000auction1\u00009e445520af04c8a3c05b484408f0406c9b09503903a9ed9b093f5d894f83dfea\u0000": { + "org": "Org1MSP", + "hash": "584dbad2269a44afb42bdbc7a7a4c08a7cd50deece1eb3fc38d4e49b9342f270" + }, + "\u0000bid\u0000auction1\u0000f3304fd81664f304706ff39b5a978af94bc3e055e5bf32aed1c25b35df24c136\u0000": { + "org": "Org1MSP", + "hash": "bbcd0c7c376e6681a76d8c5482c97f8bdcda55c90c5478100c3aef17815c4fd3" + } + }, + "revealedBids": { + "\u0000bid\u0000auction1\u00009e445520af04c8a3c05b484408f0406c9b09503903a9ed9b093f5d894f83dfea\u0000": { + "objectType": "bid", + "quantity": 1, + "price": 800, + "org": "Org1MSP", + "buyer": "eDUwOTo6Q049YmlkZGVyMSxPVT1jbGllbnQrT1U9b3JnMStPVT1kZXBhcnRtZW50MTo6Q049Y2Eub3JnMS5leGFtcGxlLmNvbSxPPW9yZzEuZXhhbXBsZS5jb20sTD1EdXJoYW0sU1Q9Tm9ydGggQ2Fyb2xpbmEsQz1VUw==" + } + }, + "winners": [], + "price": 0, + "status": "closed" +} +``` + +Bidder3 from Org2 will also reveal their bid: +``` +node revealBid.js org2 bidder3 auction1 $BIDDER3_BID_ID +``` + +If the action ended now, Bidder1 would win the auction. Let's try to end the auction using the seller identity and see what happens. + +``` +node endAuction.js org1 seller auction1 +``` + +Instead of ending the auction, the transaction results in an endorsement policy failure. The end of the auction needs to be endorsed by Org2. Before endorsing the transaction, the Org2 peer queries the Org2 implicit data collection to look for a winning bid that has not yet been revealed. Because Bidder4 created a bid that is above the winning price, the Org2 peer refuses to endorse the transaction that ends the auction. + +Before we can end the auction, we need to reveal the bid from bidder4. +``` +node revealBid.js org2 bidder4 auction1 $BIDDER4_BID_ID +``` + +Bidder2 from Org1 would not win the auction in either case. As a result, Bidder2 decides not to reveal their bid. + +## End the auction + +Now that the winning bids have been revealed, we can end the auction: +``` +node endAuction org1 seller auction1 +``` + +The transaction was successfully endorsed by both Org1 and Org2, who both calculated the same price and winner of the auction. +``` +*** Result: Auction: { + "objectType": "auction", + "item": "painting", + "seller": "eDUwOTo6Q049c2VsbGVyLE9VPWNsaWVudCtPVT1vcmcxK09VPWRlcGFydG1lbnQxOjpDTj1jYS5vcmcxLmV4YW1wbGUuY29tLE89b3JnMS5leGFtcGxlLmNvbSxMPUR1cmhhbSxTVD1Ob3J0aCBDYXJvbGluYSxDPVVT", + "quantity": 1, + "organizations": [ + "Org1MSP", + "Org2MSP" + ], + "privateBids": { + "\u0000bid\u0000auction1\u000003382872f8f9dc94f211385d3d127f155e22de1bee8e112dcb90beb3e78f2722\u0000": { + "org": "Org1MSP", + "hash": "9345d28bb40b7026cc9e14743147356ae1cac99ced8190fe72c9be69fbfc4b71" + }, + "\u0000bid\u0000auction1\u0000347e08fbbf766a0f3678c3c6e05b0613026fa4d4619ad80869773938ed893f8d\u0000": { + "org": "Org2MSP", + "hash": "ecaf72ecd6daf27889434491b0e5580c94bd682d8b0283c142b085d51fcf1f69" + }, + "\u0000bid\u0000auction1\u0000e624a5f04fb8878e0d20bf10649f06113771eba6ea4c58f46d17264d1b492a46\u0000": { + "org": "Org1MSP", + "hash": "8434bfe683c4436484176e6c9b21a5d684c0710fce9fd2506af4e05bb0f8a3ea" + }, + "\u0000bid\u0000auction1\u0000f67eed2c00812d02482d40a652d353c505f3161cd6837077443ee2f425413f5c\u0000": { + "org": "Org2MSP", + "hash": "0c49d016c894b5623bc0be211409ebb0561c272c65f9faf2959002b0168b238c" + } + }, + "revealedBids": { + "\u0000bid\u0000auction1\u000003382872f8f9dc94f211385d3d127f155e22de1bee8e112dcb90beb3e78f2722\u0000": { + "objectType": "bid", + "quantity": 1, + "price": 800, + "org": "Org1MSP", + "buyer": "eDUwOTo6Q049YmlkZGVyMSxPVT1jbGllbnQrT1U9b3JnMStPVT1kZXBhcnRtZW50MTo6Q049Y2Eub3JnMS5leGFtcGxlLmNvbSxPPW9yZzEuZXhhbXBsZS5jb20sTD1EdXJoYW0sU1Q9Tm9ydGggQ2Fyb2xpbmEsQz1VUw==" + }, + "\u0000bid\u0000auction1\u0000347e08fbbf766a0f3678c3c6e05b0613026fa4d4619ad80869773938ed893f8d\u0000": { + "objectType": "bid", + "quantity": 1, + "price": 700, + "org": "Org2MSP", + "buyer": "eDUwOTo6Q049YmlkZGVyMyxPVT1jbGllbnQrT1U9b3JnMitPVT1kZXBhcnRtZW50MTo6Q049Y2Eub3JnMi5leGFtcGxlLmNvbSxPPW9yZzIuZXhhbXBsZS5jb20sTD1IdXJzbGV5LFNUPUhhbXBzaGlyZSxDPVVL" + }, + "\u0000bid\u0000auction1\u0000f67eed2c00812d02482d40a652d353c505f3161cd6837077443ee2f425413f5c\u0000": { + "objectType": "bid", + "quantity": 1, + "price": 900, + "org": "Org2MSP", + "buyer": "eDUwOTo6Q049YmlkZGVyNCxPVT1jbGllbnQrT1U9b3JnMitPVT1kZXBhcnRtZW50MTo6Q049Y2Eub3JnMi5leGFtcGxlLmNvbSxPPW9yZzIuZXhhbXBsZS5jb20sTD1IdXJzbGV5LFNUPUhhbXBzaGlyZSxDPVVL" + } + }, + "winners": [ + { + "buyer": "eDUwOTo6Q049YmlkZGVyNCxPVT1jbGllbnQrT1U9b3JnMitPVT1kZXBhcnRtZW50MTo6Q049Y2Eub3JnMi5leGFtcGxlLmNvbSxPPW9yZzIuZXhhbXBsZS5jb20sTD1IdXJzbGV5LFNUPUhhbXBzaGlyZSxDPVVL", + "quantity": 1 + } + ], + "price": 900, + "status": "ended" +} +``` + +## Clean up + +When your are done using the auction smart contract, you can bring down the network and clean up the environment. In the `auction/application-javascript` directory, run the following command to remove the wallets used to run the applications: +``` +rm -rf wallet +``` + +You can then navigate to the test network directory and bring down the network: +```` +cd ../../test-network/ +./network.sh down +```` diff --git a/auction/application-javascript/bid.js b/auction/application-javascript/bid.js new file mode 100644 index 00000000..b78ebd2c --- /dev/null +++ b/auction/application-javascript/bid.js @@ -0,0 +1,114 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; + +const { Gateway, Wallets } = require('fabric-network'); +const path = require('path'); +const { buildCCPOrg1, buildCCPOrg2, buildWallet } = require('../../test-application/javascript/AppUtil.js'); + +const myChannel = 'mychannel'; +const myChaincodeName = 'auction'; + + +function prettyJSONString(inputString) { + if (inputString) { + return JSON.stringify(JSON.parse(inputString), null, 2); + } + else { + return inputString; + } +} + +async function bid(ccp,wallet,user,orgMSP,auctionID,quantity,price) { + try { + + const gateway = new Gateway(); + //connect using Discovery enabled + + await gateway.connect(ccp, + { wallet: wallet, identity: user, discovery: { enabled: true, asLocalhost: true } }); + + const network = await gateway.getNetwork(myChannel); + const contract = network.getContract(myChaincodeName); + + console.log('\n--> Evaluate Transaction: get your client ID'); + let buyer = await contract.evaluateTransaction('GetID'); + console.log('*** Result: Buyer ID is ' + buyer.toString()); + + let bidData = { objectType: 'bid', quantity: parseInt(quantity) , price: parseInt(price), org: orgMSP, buyer: buyer.toString()}; + + let statefulTxn = contract.createTransaction('Bid'); + statefulTxn.setEndorsingOrganizations(orgMSP); + let tmapData = Buffer.from(JSON.stringify(bidData)); + statefulTxn.setTransient({ + bid: tmapData + }); + + let bidID = statefulTxn.getTransactionId(); + + console.log('\n--> Submit Transaction: Create the bid that is stored in your private data collection of your organization'); + await statefulTxn.submit(auctionID); + console.log('*** Result: committed'); + console.log('*** Result ***SAVE THIS VALUE*** BidID: ' + bidID.toString()); + + console.log('\n--> Evaluate Transaction: read the bid that was just created'); + let result = await contract.evaluateTransaction('QueryBid',auctionID,bidID); + console.log('*** Result: Bid: ' + prettyJSONString(result.toString())); + + gateway.disconnect(); + } catch (error) { + console.error(`******** FAILED to submit bid: ${error}`); + if (error.stack) { + console.error(error.stack); + } + process.exit(1); + } +} + +async function main() { + try { + + if (process.argv[2] == undefined || process.argv[3] == undefined + || process.argv[4] == undefined || process.argv[5] == undefined + || process.argv[6] == undefined) { + console.log("Usage: node bid.js org userID auctionID quantity price"); + process.exit(1); + } + + const org = process.argv[2] + const user = process.argv[3]; + const auctionID = process.argv[4]; + const quantity = process.argv[5]; + const price = process.argv[6]; + + if (org == 'Org1' || org == 'org1') { + + const orgMSP = 'Org1MSP'; + const ccp = buildCCPOrg1(); + const walletPath = path.join(__dirname, 'wallet/org1'); + const wallet = await buildWallet(Wallets, walletPath); + await bid(ccp,wallet,user,orgMSP,auctionID,quantity,price); + } + else if (org == 'Org2' || org == 'org2') { + + const orgMSP = 'Org2MSP'; + const ccp = buildCCPOrg2(); + const walletPath = path.join(__dirname, 'wallet/org2'); + const wallet = await buildWallet(Wallets, walletPath); + await bid(ccp,wallet,user,orgMSP,auctionID,quantity,price); + } else { + console.log("Usage: node bid.js org userID auctionID quantity price"); + console.log("Org must be Org1 or Org2"); + } + } catch (error) { + console.error(`******** FAILED to run the application: ${error}`); + process.exit(1); + } +} + + +main(); diff --git a/auction/application-javascript/closeAuction.js b/auction/application-javascript/closeAuction.js new file mode 100644 index 00000000..7934fbf5 --- /dev/null +++ b/auction/application-javascript/closeAuction.js @@ -0,0 +1,109 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; + +const { Gateway, Wallets } = require('fabric-network'); +const path = require('path'); +const { buildCCPOrg1, buildCCPOrg2, buildWallet } = require('../../test-application/javascript/AppUtil.js'); + +const myChannel = 'mychannel'; +const myChaincodeName = 'auction'; + + +function prettyJSONString(inputString) { + if (inputString) { + return JSON.stringify(JSON.parse(inputString), null, 2); + } + else { + return inputString; + } +} + +async function closeAuction(ccp,wallet,user,auctionID) { + try { + + const gateway = new Gateway(); + //connect using Discovery enabled + + await gateway.connect(ccp, + { wallet: wallet, identity: user, discovery: { enabled: true, asLocalhost: true } }); + + const network = await gateway.getNetwork(myChannel); + const contract = network.getContract(myChaincodeName); + + // Query the auction to get the list of endorsing orgs. + //console.log('\n--> Evaluate Transaction: query the auction you want to close'); + let auctionString = await contract.evaluateTransaction('QueryAuction',auctionID); + //console.log('*** Result: Bid: ' + prettyJSONString(auctionString.toString())); + var auctionJSON = JSON.parse(auctionString); + + let statefulTxn = contract.createTransaction('CloseAuction'); + + if (auctionJSON.organizations.length == 2) { + statefulTxn.setEndorsingOrganizations(auctionJSON.organizations[0],auctionJSON.organizations[1]); + } else { + statefulTxn.setEndorsingOrganizations(auctionJSON.organizations[0]); + } + + console.log('\n--> Submit Transaction: close auction'); + await statefulTxn.submit(auctionID); + console.log('*** Result: committed'); + + console.log('\n--> Evaluate Transaction: query the updated auction'); + let result = await contract.evaluateTransaction('QueryAuction',auctionID); + console.log('*** Result: Auction: ' + prettyJSONString(result.toString())); + + gateway.disconnect(); + } catch (error) { + console.error(`******** FAILED to submit bid: ${error}`); + process.exit(1); + } +} + +async function main() { + try { + + if (process.argv[2] == undefined || process.argv[3] == undefined + || process.argv[4] == undefined) { + console.log("Usage: node closeAuction.js org userID auctionID"); + process.exit(1); + } + + const org = process.argv[2] + const user = process.argv[3]; + const auctionID = process.argv[4]; + + if (org == 'Org1' || org == 'org1') { + + const orgMSP = 'Org1MSP'; + const ccp = buildCCPOrg1(); + const walletPath = path.join(__dirname, 'wallet/org1'); + const wallet = await buildWallet(Wallets, walletPath); + await closeAuction(ccp,wallet,user,auctionID); + } + else if (org == 'Org2' || org == 'org2') { + + const orgMSP = 'Org2MSP'; + const ccp = buildCCPOrg2(); + const walletPath = path.join(__dirname, 'wallet/org2'); + const wallet = await buildWallet(Wallets, walletPath); + await closeAuction(ccp,wallet,user,auctionID); + } else { + console.log("Usage: node closeAuction.js org userID auctionID "); + console.log("Org must be Org1 or Org2"); + } + } catch (error) { + console.error(`******** FAILED to run the application: ${error}`); + if (error.stack) { + console.error(error.stack); + } + process.exit(1); + } +} + + +main(); diff --git a/auction/application-javascript/createAuction.js b/auction/application-javascript/createAuction.js new file mode 100644 index 00000000..ca47e9d5 --- /dev/null +++ b/auction/application-javascript/createAuction.js @@ -0,0 +1,95 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; + +const { Gateway, Wallets } = require('fabric-network'); +const path = require('path'); +const { buildCCPOrg1, buildCCPOrg2, buildWallet } = require('../../test-application/javascript/AppUtil.js'); + +const myChannel = 'mychannel'; +const myChaincodeName = 'auction'; + + +function prettyJSONString(inputString) { + if (inputString) { + return JSON.stringify(JSON.parse(inputString), null, 2); + } + else { + return inputString; + } +} + +async function createAuction(ccp,wallet,user,auctionID,item,quantity) { + try { + + const gateway = new Gateway(); + //connect using Discovery enabled + + await gateway.connect(ccp, + { wallet: wallet, identity: user, discovery: { enabled: true, asLocalhost: true } }); + + const network = await gateway.getNetwork(myChannel); + const contract = network.getContract(myChaincodeName); + + let statefulTxn = contract.createTransaction('CreateAuction'); + + console.log('\n--> Submit Transaction: Propose a new auction'); + await statefulTxn.submit(auctionID,item,parseInt(quantity)); + console.log('*** Result: committed'); + + console.log('\n--> Evaluate Transaction: query the auction that was just created'); + let result = await contract.evaluateTransaction('QueryAuction',auctionID); + console.log('*** Result: Auction: ' + prettyJSONString(result.toString())); + + gateway.disconnect(); + } catch (error) { + console.error(`******** FAILED to submit bid: ${error}`); + } +} + +async function main() { + try { + + if (process.argv[2] == undefined || process.argv[3] == undefined + || process.argv[4] == undefined || process.argv[5] == undefined + || process.argv[6] == undefined) { + console.log("Usage: node createAuction.js org userID auctionID item quantity"); + process.exit(1); + } + + const org = process.argv[2] + const user = process.argv[3]; + const auctionID = process.argv[4]; + const item = process.argv[5]; + const quantity = process.argv[6]; + + if (org == 'Org1' || org == 'org1') { + + const orgMSP = 'Org1MSP'; + const ccp = buildCCPOrg1(); + const walletPath = path.join(__dirname, 'wallet/org1'); + const wallet = await buildWallet(Wallets, walletPath); + await createAuction(ccp,wallet,user,auctionID,item,quantity); + } + else if (org == 'Org2' || org == 'org2') { + + const orgMSP = 'Org2MSP'; + const ccp = buildCCPOrg2(); + const walletPath = path.join(__dirname, 'wallet/org2'); + const wallet = await buildWallet(Wallets, walletPath); + await createAuction(ccp,wallet,user,auctionID,item,quantity); + } else { + console.log("Usage: node createAuction.js org userID auctionID item quantity"); + console.log("Org must be Org1 or Org2"); + } + } catch (error) { + console.error(`******** FAILED to run the application: ${error}`); + } +} + + +main(); diff --git a/auction/application-javascript/endAuction.js b/auction/application-javascript/endAuction.js new file mode 100644 index 00000000..3c16c7fc --- /dev/null +++ b/auction/application-javascript/endAuction.js @@ -0,0 +1,109 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; + +const { Gateway, Wallets } = require('fabric-network'); +const path = require('path'); +const { buildCCPOrg1, buildCCPOrg2, buildWallet } = require('../../test-application/javascript/AppUtil.js'); + +const myChannel = 'mychannel'; +const myChaincodeName = 'auction'; + + +function prettyJSONString(inputString) { + if (inputString) { + return JSON.stringify(JSON.parse(inputString), null, 2); + } + else { + return inputString; + } +} + +async function endAuction(ccp,wallet,user,auctionID) { + try { + + const gateway = new Gateway(); + //connect using Discovery enabled + + await gateway.connect(ccp, + { wallet: wallet, identity: user, discovery: { enabled: true, asLocalhost: true } }); + + const network = await gateway.getNetwork(myChannel); + const contract = network.getContract(myChaincodeName); + + // Query the auction to get the list of endorsing orgs. + //console.log('\n--> Evaluate Transaction: query the auction you want to end'); + let auctionString = await contract.evaluateTransaction('QueryAuction',auctionID); + //console.log('*** Result: Bid: ' + prettyJSONString(auctionString.toString())); + var auctionJSON = JSON.parse(auctionString); + + let statefulTxn = contract.createTransaction('EndAuction'); + + if (auctionJSON.organizations.length == 2) { + statefulTxn.setEndorsingOrganizations(auctionJSON.organizations[0],auctionJSON.organizations[1]); + } else { + statefulTxn.setEndorsingOrganizations(auctionJSON.organizations[0]); + } + + console.log('\n--> Submit the transaction to end the auction'); + await statefulTxn.submit(auctionID); + console.log('*** Result: committed'); + + console.log('\n--> Evaluate Transaction: query the updated auction'); + let result = await contract.evaluateTransaction('QueryAuction',auctionID); + console.log('*** Result: Auction: ' + prettyJSONString(result.toString())); + + gateway.disconnect(); + } catch (error) { + console.error(`******** FAILED to submit bid: ${error}`); + process.exit(1); + } +} + +async function main() { + try { + + if (process.argv[2] == undefined || process.argv[3] == undefined + || process.argv[4] == undefined) { + console.log("Usage: node endAuction.js org userID auctionID"); + process.exit(1); + } + + const org = process.argv[2] + const user = process.argv[3]; + const auctionID = process.argv[4]; + + if (org == 'Org1' || org == 'org1') { + + const orgMSP = 'Org1MSP'; + const ccp = buildCCPOrg1(); + const walletPath = path.join(__dirname, 'wallet/org1'); + const wallet = await buildWallet(Wallets, walletPath); + await endAuction(ccp,wallet,user,auctionID); + } + else if (org == 'Org2' || org == 'org2') { + + const orgMSP = 'Org2MSP'; + const ccp = buildCCPOrg2(); + const walletPath = path.join(__dirname, 'wallet/org2'); + const wallet = await buildWallet(Wallets, walletPath); + await endAuction(ccp,wallet,user,auctionID); + } else { + console.log("Usage: node endAuction.js org userID auctionID"); + console.log("Org must be Org1 or Org2"); + } + } catch (error) { + console.error(`******** FAILED to run the application: ${error}`); + if (error.stack) { + console.error(error.stack); + } + process.exit(1); + } +} + + +main(); diff --git a/auction/application-javascript/enrollAdmin.js b/auction/application-javascript/enrollAdmin.js new file mode 100644 index 00000000..e2df9cfb --- /dev/null +++ b/auction/application-javascript/enrollAdmin.js @@ -0,0 +1,76 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; + +const { Wallets } = require('fabric-network'); +const FabricCAServices = require('fabric-ca-client'); +const path = require('path'); +const { buildCAClient, enrollAdmin } = require('../../test-application/javascript/CAUtil.js'); +const { buildCCPOrg1, buildCCPOrg2, buildWallet } = require('../../test-application/javascript/AppUtil.js'); + +const mspOrg1 = 'Org1MSP'; +const mspOrg2 = 'Org2MSP'; + +function prettyJSONString(inputString) { + if (inputString) { + return JSON.stringify(JSON.parse(inputString), null, 2); + } + else { + return inputString; + } +} + +async function connectToOrg1CA() { + console.log('\n--> Enrolling the Org1 CA admin'); + const ccpOrg1 = buildCCPOrg1(); + const caOrg1Client = buildCAClient(FabricCAServices, ccpOrg1, 'ca.org1.example.com'); + + const walletPathOrg1 = path.join(__dirname, 'wallet/org1'); + const walletOrg1 = await buildWallet(Wallets, walletPathOrg1); + + await enrollAdmin(caOrg1Client, walletOrg1, mspOrg1); + +} + +async function connectToOrg2CA() { + console.log('\n--> Enrolling the Org2 CA admin'); + 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); + +} +async function main() { + + if (process.argv[2] == undefined) { + console.log("Usage: node enrollAdmin.js Org"); + process.exit(1); + } + + const org = process.argv[2]; + + try { + + if (org == 'Org1' || org == 'org1') { + await connectToOrg1CA(); + } + else if (org == 'Org2' || org == 'org2') { + await connectToOrg2CA(); + } else { + console.log("Usage: node registerUser.js org userID"); + console.log("Org must be Org1 or Org2"); + } + } catch (error) { + console.error(`Error in enrolling admin: ${error}`); + process.exit(1); + } +} + +main(); diff --git a/auction/application-javascript/package.json b/auction/application-javascript/package.json new file mode 100644 index 00000000..b9d531c1 --- /dev/null +++ b/auction/application-javascript/package.json @@ -0,0 +1,16 @@ +{ + "name": "auction", + "version": "1.0.0", + "description": "auction application implemented in JavaScript", + "engines": { + "node": ">=12", + "npm": ">=5" + }, + "engineStrict": true, + "author": "Hyperledger", + "license": "Apache-2.0", + "dependencies": { + "fabric-ca-client": "^2.2.0", + "fabric-network": "^2.2.0" + } +} diff --git a/auction/application-javascript/queryAuction.js b/auction/application-javascript/queryAuction.js new file mode 100644 index 00000000..258d19c1 --- /dev/null +++ b/auction/application-javascript/queryAuction.js @@ -0,0 +1,86 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; + +const { Gateway, Wallets } = require('fabric-network'); +const path = require('path'); +const { buildCCPOrg1, buildCCPOrg2, buildWallet } = require('../../test-application/javascript/AppUtil.js'); + +const myChannel = 'mychannel'; +const myChaincodeName = 'auction'; + + +function prettyJSONString(inputString) { + if (inputString) { + return JSON.stringify(JSON.parse(inputString), null, 2); + } + else { + return inputString; + } +} + +async function queryAuction(ccp,wallet,user,auctionID) { + try { + + const gateway = new Gateway(); + //connect using Discovery enabled + + await gateway.connect(ccp, + { wallet: wallet, identity: user, discovery: { enabled: true, asLocalhost: true } }); + + const network = await gateway.getNetwork(myChannel); + const contract = network.getContract(myChaincodeName); + + console.log('\n--> Evaluate Transaction: query the auction'); + let result = await contract.evaluateTransaction('QueryAuction',auctionID); + console.log('*** Result: Auction: ' + prettyJSONString(result.toString())); + + gateway.disconnect(); + } catch (error) { + console.error(`******** FAILED to submit bid: ${error}`); + } +} + +async function main() { + try { + + if (process.argv[2] == undefined || process.argv[3] == undefined + || process.argv[4] == undefined) { + console.log("Usage: node queryAuction.js org userID auctionID"); + process.exit(1); + } + + const org = process.argv[2] + const user = process.argv[3]; + const auctionID = process.argv[4]; + + if (org == 'Org1' || org == 'org1') { + + const orgMSP = 'Org1MSP'; + const ccp = buildCCPOrg1(); + const walletPath = path.join(__dirname, 'wallet/org1'); + const wallet = await buildWallet(Wallets, walletPath); + await queryAuction(ccp,wallet,user,auctionID); + } + else if (org == 'Org2' || org == 'org2') { + + const orgMSP = 'Org2MSP'; + const ccp = buildCCPOrg2(); + const walletPath = path.join(__dirname, 'wallet/org2'); + const wallet = await buildWallet(Wallets, walletPath); + await queryAuction(ccp,wallet,user,auctionID); + } else { + console.log("Usage: node queryAuction.js org userID auctionID"); + console.log("Org must be Org1 or Org2"); + } + } catch (error) { + console.error(`******** FAILED to run the application: ${error}`); + } +} + + +main(); diff --git a/auction/application-javascript/queryBid.js b/auction/application-javascript/queryBid.js new file mode 100644 index 00000000..b4ffea69 --- /dev/null +++ b/auction/application-javascript/queryBid.js @@ -0,0 +1,87 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; + +const { Gateway, Wallets } = require('fabric-network'); +const path = require('path'); +const { buildCCPOrg1, buildCCPOrg2, buildWallet } = require('../../test-application/javascript/AppUtil.js'); + +const myChannel = 'mychannel'; +const myChaincodeName = 'auction'; + + +function prettyJSONString(inputString) { + if (inputString) { + return JSON.stringify(JSON.parse(inputString), null, 2); + } + else { + return inputString; + } +} + +async function queryBid(ccp,wallet,user,auctionID,bidID) { + try { + + const gateway = new Gateway(); + //connect using Discovery enabled + + await gateway.connect(ccp, + { wallet: wallet, identity: user, discovery: { enabled: true, asLocalhost: true } }); + + const network = await gateway.getNetwork(myChannel); + const contract = network.getContract(myChaincodeName); + + console.log('\n--> Evaluate Transaction: read bid from private data store'); + let result = await contract.evaluateTransaction('QueryBid',auctionID,bidID); + console.log('*** Result: Bid: ' + prettyJSONString(result.toString())); + + gateway.disconnect(); + } catch (error) { + console.error(`******** FAILED to submit bid: ${error}`); + } +} + +async function main() { + try { + + if (process.argv[2] == undefined || process.argv[3] == undefined + || process.argv[4] == undefined || process.argv[5] == undefined) { + console.log("Usage: node bid.js org userID auctionID bidID"); + process.exit(1); + } + + const org = process.argv[2] + const user = process.argv[3]; + const auctionID = process.argv[4]; + const bidID = process.argv[5]; + + if (org == 'Org1' || org == 'org1') { + + const orgMSP = 'Org1MSP'; + const ccp = buildCCPOrg1(); + const walletPath = path.join(__dirname, 'wallet/org1'); + const wallet = await buildWallet(Wallets, walletPath); + await queryBid(ccp,wallet,user,auctionID,bidID); + } + else if (org == 'Org2' || org == 'org2') { + + const orgMSP = 'Org2MSP'; + const ccp = buildCCPOrg2(); + const walletPath = path.join(__dirname, 'wallet/org2'); + const wallet = await buildWallet(Wallets, walletPath); + await queryBid(ccp,wallet,user,auctionID,bidID); + } else { + console.log("Usage: node bid.js org userID auctionID bidID"); + console.log("Org must be Org1 or Org2"); + } + } catch (error) { + console.error(`******** FAILED to run the application: ${error}`); + } +} + + +main(); diff --git a/auction/application-javascript/registerEnrollUser.js b/auction/application-javascript/registerEnrollUser.js new file mode 100644 index 00000000..3ee97a6a --- /dev/null +++ b/auction/application-javascript/registerEnrollUser.js @@ -0,0 +1,77 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; + +const { Wallets } = require('fabric-network'); +const FabricCAServices = require('fabric-ca-client'); +const path = require('path'); +const { buildCAClient, registerAndEnrollUser } = require('../../test-application/javascript/CAUtil.js'); +const { buildCCPOrg1, buildCCPOrg2, buildWallet } = require('../../test-application/javascript/AppUtil.js'); + +const mspOrg1 = 'Org1MSP'; +const mspOrg2 = 'Org2MSP'; + +function prettyJSONString(inputString) { + if (inputString) { + return JSON.stringify(JSON.parse(inputString), null, 2); + } + else { + return inputString; + } +} + +async function connectToOrg1CA(UserID) { + console.log('\n--> Register and enrolling new user'); + const ccpOrg1 = buildCCPOrg1(); + const caOrg1Client = buildCAClient(FabricCAServices, ccpOrg1, 'ca.org1.example.com'); + + const walletPathOrg1 = path.join(__dirname, 'wallet/org1'); + const walletOrg1 = await buildWallet(Wallets, walletPathOrg1); + + await registerAndEnrollUser(caOrg1Client, walletOrg1, mspOrg1, UserID, 'org1.department1'); + +} + +async function connectToOrg2CA(UserID) { + console.log('\n--> Register and enrolling new user'); + 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 registerAndEnrollUser(caOrg2Client, walletOrg2, mspOrg2, UserID, 'org2.department1'); + +} +async function main() { + + if (process.argv[2] == undefined && process.argv[3] == undefined) { + console.log("Usage: node registerEnrollUser.js org userID"); + process.exit(1); + } + + const org = process.argv[2]; + const userId = process.argv[3]; + + try { + + if (org == 'Org1' || org == 'org1') { + await connectToOrg1CA(userId); + } + else if (org == 'Org2' || org == 'org2') { + await connectToOrg2CA(userId); + } else { + console.log("Usage: node registerEnrollUser.js org userID"); + console.log("Org must be Org1 or Org2"); + } + } catch (error) { + console.error(`Error in enrolling admin: ${error}`); + process.exit(1); + } +} + +main(); diff --git a/auction/application-javascript/revealBid.js b/auction/application-javascript/revealBid.js new file mode 100644 index 00000000..ce761748 --- /dev/null +++ b/auction/application-javascript/revealBid.js @@ -0,0 +1,119 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; + +const { Gateway, Wallets } = require('fabric-network'); +const path = require('path'); +const { buildCCPOrg1, buildCCPOrg2, buildWallet } = require('../../test-application/javascript/AppUtil.js'); + +const myChannel = 'mychannel'; +const myChaincodeName = 'auction'; + + +function prettyJSONString(inputString) { + if (inputString) { + return JSON.stringify(JSON.parse(inputString), null, 2); + } + else { + return inputString; + } +} + +async function addBid(ccp,wallet,user,auctionID,bidID) { + try { + + const gateway = new Gateway(); + //connect using Discovery enabled + + await gateway.connect(ccp, + { wallet: wallet, identity: user, discovery: { enabled: true, asLocalhost: true } }); + + const network = await gateway.getNetwork(myChannel); + const contract = network.getContract(myChaincodeName); + + console.log('\n--> Evaluate Transaction: read your bid'); + let bidString = await contract.evaluateTransaction('QueryBid',auctionID,bidID); + var bidJSON = JSON.parse(bidString); + + //console.log('\n--> Evaluate Transaction: query the auction you want to join'); + let auctionString = await contract.evaluateTransaction('QueryAuction',auctionID); + // console.log('*** Result: Bid: ' + prettyJSONString(auctionString.toString())); + var auctionJSON = JSON.parse(auctionString); + + let bidData = { objectType: 'bid', quantity: parseInt(bidJSON.quantity) , price: parseInt(bidJSON.price), org: bidJSON.org, buyer: bidJSON.buyer}; + console.log('*** Result: Bid: ' + JSON.stringify(bidData,null,2)); + + let statefulTxn = contract.createTransaction('RevealBid'); + let tmapData = Buffer.from(JSON.stringify(bidData)); + statefulTxn.setTransient({ + bid: tmapData + }); + + if (auctionJSON.organizations.length == 2) { + statefulTxn.setEndorsingOrganizations(auctionJSON.organizations[0],auctionJSON.organizations[1]); + } else { + statefulTxn.setEndorsingOrganizations(auctionJSON.organizations[0]); + } + + await statefulTxn.submit(auctionID,bidID); + + console.log('\n--> Evaluate Transaction: query the auction to see that our bid was added'); + let result = await contract.evaluateTransaction('QueryAuction',auctionID); + console.log('*** Result: Auction: ' + prettyJSONString(result.toString())); + + gateway.disconnect(); + } catch (error) { + console.error(`******** FAILED to submit bid: ${error}`); + process.exit(1); + } +} + +async function main() { + try { + + if (process.argv[2] == undefined || process.argv[3] == undefined + || process.argv[4] == undefined || process.argv[5] == undefined) { + console.log("Usage: node revealBid.js org userID auctionID bidID"); + process.exit(1); + } + + const org = process.argv[2] + const user = process.argv[3]; + const auctionID = process.argv[4]; + const bidID = process.argv[5]; + + if (org == 'Org1' || org == 'org1') { + + const orgMSP = 'Org1MSP'; + const ccp = buildCCPOrg1(); + const walletPath = path.join(__dirname, 'wallet/org1'); + const wallet = await buildWallet(Wallets, walletPath); + await addBid(ccp,wallet,user,auctionID,bidID); + } + else if (org == 'Org2' || org == 'org2') { + + const orgMSP = 'Org2MSP'; + const ccp = buildCCPOrg2(); + const walletPath = path.join(__dirname, 'wallet/org2'); + const wallet = await buildWallet(Wallets, walletPath); + await addBid(ccp,wallet,user,auctionID,bidID); + } + else { + console.log("Usage: node revealBid.js org userID auctionID bidID"); + console.log("Org must be Org1 or Org2"); + } + } catch (error) { + console.error(`******** FAILED to run the application: ${error}`); + if (error.stack) { + console.error(error.stack); + } + process.exit(1); + } +} + + +main(); diff --git a/auction/application-javascript/submitBid.js b/auction/application-javascript/submitBid.js new file mode 100644 index 00000000..ef6cf2a4 --- /dev/null +++ b/auction/application-javascript/submitBid.js @@ -0,0 +1,108 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; + +const { Gateway, Wallets } = require('fabric-network'); +const path = require('path'); +const { buildCCPOrg1, buildCCPOrg2, buildWallet } = require('../../test-application/javascript/AppUtil.js'); + +const myChannel = 'mychannel'; +const myChaincodeName = 'auction'; + + +function prettyJSONString(inputString) { + if (inputString) { + return JSON.stringify(JSON.parse(inputString), null, 2); + } + else { + return inputString; + } +} + +async function submitBid(ccp,wallet,user,auctionID,bidID) { + try { + + const gateway = new Gateway(); + //connect using Discovery enabled + + await gateway.connect(ccp, + { wallet: wallet, identity: user, discovery: { enabled: true, asLocalhost: true } }); + + const network = await gateway.getNetwork(myChannel); + const contract = network.getContract(myChaincodeName); + + console.log('\n--> Evaluate Transaction: query the auction you want to join'); + let auctionString = await contract.evaluateTransaction('QueryAuction',auctionID); + var auctionJSON = JSON.parse(auctionString); + + let statefulTxn = contract.createTransaction('SubmitBid'); + + if (auctionJSON.organizations.length == 2) { + statefulTxn.setEndorsingOrganizations(auctionJSON.organizations[0],auctionJSON.organizations[1]); + } else { + statefulTxn.setEndorsingOrganizations(auctionJSON.organizations[0]); + } + + console.log('\n--> Submit Transaction: add bid to the auction'); + await statefulTxn.submit(auctionID,bidID); + + console.log('\n--> Evaluate Transaction: query the auction to see that our bid was added'); + let result = await contract.evaluateTransaction('QueryAuction',auctionID); + console.log('*** Result: Auction: ' + prettyJSONString(result.toString())); + + gateway.disconnect(); + } catch (error) { + console.error(`******** FAILED to submit bid: ${error}`); + process.exit(1); + } +} + +async function main() { + try { + + if (process.argv[2] == undefined || process.argv[3] == undefined + || process.argv[4] == undefined || process.argv[5] == undefined) { + console.log("Usage: node submitBid.js org userID auctionID bidID"); + process.exit(1); + } + + const org = process.argv[2] + const user = process.argv[3]; + const auctionID = process.argv[4]; + const bidID = process.argv[5]; + + if (org == 'Org1' || org == 'org1') { + + const orgMSP = 'Org1MSP'; + const ccp = buildCCPOrg1(); + const walletPath = path.join(__dirname, 'wallet/org1'); + const wallet = await buildWallet(Wallets, walletPath); + await submitBid(ccp,wallet,user,auctionID,bidID); + } + else if (org == 'Org2' || org == 'org2') { + + const orgMSP = 'Org2MSP'; + const ccp = buildCCPOrg2(); + const walletPath = path.join(__dirname, 'wallet/org2'); + const wallet = await buildWallet(Wallets, walletPath); + await submitBid(ccp,wallet,user,auctionID,bidID); + } + else { + console.log("Usage: node submitBid.js org userID auctionID bidID"); + console.log("Org must be Org1 or Org2"); + } + } catch (error) { + console.error(`******** FAILED to run the application: ${error}`); + if (error.stack) { + console.error(error.stack); + } + process.exit(1); + } +} + + +main(); diff --git a/auction/chaincode-go/go.mod b/auction/chaincode-go/go.mod new file mode 100644 index 00000000..26f43afa --- /dev/null +++ b/auction/chaincode-go/go.mod @@ -0,0 +1,8 @@ +module github.com/hyperledger/fabric-samples/auction/chaincode-go + +go 1.15 + +require ( + github.com/hyperledger/fabric-chaincode-go v0.0.0-20200728190242-9b3ae92d8664 + github.com/hyperledger/fabric-contract-api-go v1.1.0 +) diff --git a/auction/chaincode-go/go.sum b/auction/chaincode-go/go.sum new file mode 100644 index 00000000..f052c527 --- /dev/null +++ b/auction/chaincode-go/go.sum @@ -0,0 +1,139 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/DATA-DOG/go-txdb v0.1.3/go.mod h1:DhAhxMXZpUJVGnT+p9IbzJoRKvlArO2pkHjnGX7o0n0= +github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/cucumber/godog v0.8.0/go.mod h1:Cp3tEV1LRAyH/RuCThcxHS/+9ORZ+FMzPva2AZ5Ki+A= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= +github.com/go-openapi/jsonpointer v0.19.3 h1:gihV7YNZK1iK6Tgwwsxo2rJbD1GTbdm72325Bq8FI3w= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.19.2 h1:o20suLFB4Ri0tuzpWtyHlh7E7HnkqTNLq6aR6WVNS1w= +github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= +github.com/go-openapi/spec v0.19.4 h1:ixzUSnHTd6hCemgtAJgluaTSGYpLNpJY4mA2DIkdOAo= +github.com/go-openapi/spec v0.19.4/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= +github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/gobuffalo/envy v1.7.0 h1:GlXgaiBkmrYMHco6t4j7SacKO4XUjvh5pwXh0f4uxXU= +github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= +github.com/gobuffalo/logger v1.0.0/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs= +github.com/gobuffalo/packd v0.3.0 h1:eMwymTkA1uXsqxS0Tpoop3Lc0u3kTfiMBE6nKtQU4g4= +github.com/gobuffalo/packd v0.3.0/go.mod h1:zC7QkmNkYVGKPw4tHpBQ+ml7W/3tIebgeo1b36chA3Q= +github.com/gobuffalo/packr v1.30.1 h1:hu1fuVR3fXEZR7rXNW3h8rqSML8EVAf6KNm0NKO/wKg= +github.com/gobuffalo/packr v1.30.1/go.mod h1:ljMyFO2EcrnzsHsN99cvbq055Y9OhRrIaviy289eRuk= +github.com/gobuffalo/packr/v2 v2.5.1/go.mod h1:8f9c96ITobJlPzI44jj+4tHnEKNt0xXWSVlXRN9X1Iw= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hyperledger/fabric-chaincode-go v0.0.0-20200424173110-d7076418f212/go.mod h1:N7H3sA7Tx4k/YzFq7U0EPdqJtqvM4Kild0JoCc7C0Dc= +github.com/hyperledger/fabric-chaincode-go v0.0.0-20200728190242-9b3ae92d8664 h1:Pu/9SNpo71SJj5DGehCXOKD9QGQ3MsuWjpsLM9Mkdwg= +github.com/hyperledger/fabric-chaincode-go v0.0.0-20200728190242-9b3ae92d8664/go.mod h1:N7H3sA7Tx4k/YzFq7U0EPdqJtqvM4Kild0JoCc7C0Dc= +github.com/hyperledger/fabric-contract-api-go v1.1.0 h1:K9uucl/6eX3NF0/b+CGIiO1IPm1VYQxBkpnVGJur2S4= +github.com/hyperledger/fabric-contract-api-go v1.1.0/go.mod h1:nHWt0B45fK53owcFpLtAe8DH0Q5P068mnzkNXMPSL7E= +github.com/hyperledger/fabric-protos-go v0.0.0-20190919234611-2a87503ac7c9/go.mod h1:xVYTjK4DtZRBxZ2D9aE4y6AbLaPwue2o/criQyQbVD0= +github.com/hyperledger/fabric-protos-go v0.0.0-20200424173316-dd554ba3746e h1:9PS5iezHk/j7XriSlNuSQILyCOfcZ9wZ3/PiucmSE8E= +github.com/hyperledger/fabric-protos-go v0.0.0-20200424173316-dd554ba3746e/go.mod h1:xVYTjK4DtZRBxZ2D9aE4y6AbLaPwue2o/criQyQbVD0= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= +github.com/karrick/godirwalk v1.10.12/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e h1:hB2xlXdHp/pmPZq0y3QnmWAArdw9PqbmotexnWx/FU8= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.3.0 h1:RR9dF3JtopPvtkroDZuVD7qquD0bnHlKSqaQhgwt8yk= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297 h1:k7pJ2yAPLPgbskkFdhRCsA77k2fySZ1zf2zCjvQCiIM= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190710143415-6ec70d6a5542 h1:6ZQFf1D2YYDDI7eSwW8adlkkavTB9sw5I24FVtEvNUQ= +golang.org/x/sys v0.0.0-20190710143415-6ec70d6a5542/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190624180213-70d37148ca0c/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20180831171423-11092d34479b h1:lohp5blsw53GBXtLyLNaTXPXS9pJ1tiTw61ZHUoE9Qw= +google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/grpc v1.23.0 h1:AzbTB6ux+okLTzP8Ru1Xs41C303zdcfEht7MQnYJt5A= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/auction/chaincode-go/smart-contract/auction.go b/auction/chaincode-go/smart-contract/auction.go new file mode 100644 index 00000000..1f131a05 --- /dev/null +++ b/auction/chaincode-go/smart-contract/auction.go @@ -0,0 +1,539 @@ +/* +SPDX-License-Identifier: Apache-2.0 +*/ + +package auction + +import ( + "bytes" + "crypto/sha256" + "encoding/json" + "fmt" + "sort" + + "github.com/hyperledger/fabric-contract-api-go/contractapi" +) + +type SmartContract struct { + contractapi.Contract +} + +// Auction data +type Auction struct { + Type string `json:"objectType"` + ItemSold string `json:"item"` + Seller string `json:"seller"` + Quantity int `json:"quantity"` + Orgs []string `json:"organizations"` + PrivateBids map[string]BidHash `json:"privateBids"` + RevealedBids map[string]FullBid `json:"revealedBids"` + Winners []Winners `json:"winners"` + Price int `json:"price"` + Status string `json:"status"` +} + +// FullBid is the structure of a revealed bid +type FullBid struct { + Type string `json:"objectType"` + Quantity int `json:"quantity"` + Price int `json:"price"` + Org string `json:"org"` + Buyer string `json:"buyer"` +} + +// BidHash is the structure of a private bid +type BidHash struct { + Org string `json:"org"` + Hash string `json:"hash"` +} + +// Winners stores the winners of the auction +type Winners struct { + Buyer string `json:"buyer"` + Quantity int `json:"quantity"` +} + +const bidKeyType = "bid" + +// CreateAuction creates on auction on the public channel. The identity that +// submits the transacion becomes the seller of the auction +func (s *SmartContract) CreateAuction(ctx contractapi.TransactionContextInterface, auctionID string, itemsold string, quantity int) error { + + // get ID of submitting client + clientID, err := ctx.GetClientIdentity().GetID() + if err != nil { + return fmt.Errorf("failed to get client identity %v", err) + } + + // get org of submitting client + clientOrgID, err := ctx.GetClientIdentity().GetMSPID() + if err != nil { + return fmt.Errorf("failed to get client identity %v", err) + } + + // Create auction + bidders := make(map[string]BidHash) + revealedBids := make(map[string]FullBid) + + auction := Auction{ + Type: "auction", + ItemSold: itemsold, + Quantity: quantity, + Price: 0, + Seller: clientID, + Orgs: []string{clientOrgID}, + PrivateBids: bidders, + RevealedBids: revealedBids, + Winners: []Winners{}, + Status: "open", + } + + auctionBytes, err := json.Marshal(auction) + if err != nil { + return err + } + + // put auction into state + err = ctx.GetStub().PutState(auctionID, auctionBytes) + if err != nil { + return fmt.Errorf("failed to put auction in public data: %v", err) + } + + // set the seller of the auction as an endorser + err = setAssetStateBasedEndorsement(ctx, auctionID, clientOrgID) + if err != nil { + return fmt.Errorf("failed setting state based endorsement for new organization: %v", err) + } + + return nil +} + +// Bid is used to add a users bid to the auction. The bid is stored in the private +// data collection on the peer of the bidder's organization. The function returns +// the transaction ID so that users can identify and query their bid +func (s *SmartContract) Bid(ctx contractapi.TransactionContextInterface, auctionID string) (string, error) { + + // get bid from transient map + transientMap, err := ctx.GetStub().GetTransient() + if err != nil { + return "", fmt.Errorf("error getting transient: %v", err) + } + + BidJSON, ok := transientMap["bid"] + if !ok { + return "", fmt.Errorf("bid key not found in the transient map") + } + + // get the implicit collection name using the bidder's organization ID + collection, err := getCollectionName(ctx) + if err != nil { + return "", fmt.Errorf("failed to get implicit collection name: %v", err) + } + + // the bidder has to target their peer to store the bid + err = verifyClientOrgMatchesPeerOrg(ctx) + if err != nil { + return "", fmt.Errorf("Cannot store bid on this peer, not a member of this org: Error %v", err) + } + + // the transaction ID is used as a unique index for the bid + txID := ctx.GetStub().GetTxID() + + // create a composite key using the transaction ID + bidKey, err := ctx.GetStub().CreateCompositeKey(bidKeyType, []string{auctionID, txID}) + if err != nil { + return "", fmt.Errorf("failed to create composite key: %v", err) + } + + // put the bid into the organization's implicit data collection + err = ctx.GetStub().PutPrivateData(collection, bidKey, BidJSON) + if err != nil { + return "", fmt.Errorf("failed to input price into collection: %v", err) + } + + // return the trannsaction ID so that the uset can identify their bid + return txID, nil +} + +// SubmitBid is used by the bidder to add the hash of that bid stored in private data to the +// auction. Note that this function alters the auction in private state, and needs +// to meet the auction endorsement policy. Transaction ID is used identify the bid +func (s *SmartContract) SubmitBid(ctx contractapi.TransactionContextInterface, auctionID string, txID string) error { + + // get the MSP ID of the bidder's org + clientOrgID, err := ctx.GetClientIdentity().GetMSPID() + if err != nil { + return fmt.Errorf("failed to get client MSP ID: %v", err) + } + + // get the auction from state + auctionBytes, err := ctx.GetStub().GetState(auctionID) + var auctionJSON Auction + + if auctionBytes == nil { + return fmt.Errorf("Auction not found: %v", auctionID) + } + err = json.Unmarshal(auctionBytes, &auctionJSON) + if err != nil { + return fmt.Errorf("failed to create auction object JSON: %v", err) + } + + // the auction needs to be open for users to add their bid + Status := auctionJSON.Status + if Status != "open" { + return fmt.Errorf("cannot join closed or ended auction") + } + + // get the inplicit collection name of bidder's org + collection, err := getCollectionName(ctx) + if err != nil { + return fmt.Errorf("failed to get implicit collection name: %v", err) + } + + // use the transaction ID passed as a parameter to create composite bid key + bidKey, err := ctx.GetStub().CreateCompositeKey(bidKeyType, []string{auctionID, txID}) + if err != nil { + return fmt.Errorf("failed to create composite key: %v", err) + } + + // get the hash of the bid if found in private collection + bidHash, err := ctx.GetStub().GetPrivateDataHash(collection, bidKey) + if err != nil { + return fmt.Errorf("failed to read bid bash from collection: %v", err) + } + if bidHash == nil { + return fmt.Errorf("bid hash does not exist: %s", bidKey) + } + + // store the hash along with the bidder's organization + NewHash := BidHash{ + Org: clientOrgID, + Hash: fmt.Sprintf("%x", bidHash), + } + + bidders := make(map[string]BidHash) + bidders = auctionJSON.PrivateBids + bidders[bidKey] = NewHash + auctionJSON.PrivateBids = bidders + + // Add the bidding organization to the list of participating organization's if it is not already + Orgs := auctionJSON.Orgs + if !(contains(Orgs, clientOrgID)) { + newOrgs := append(Orgs, clientOrgID) + auctionJSON.Orgs = newOrgs + + err = addAssetStateBasedEndorsement(ctx, auctionID, clientOrgID) + if err != nil { + return fmt.Errorf("failed setting state based endorsement for new organization: %v", err) + } + } + + newAuctionBytes, _ := json.Marshal(auctionJSON) + + err = ctx.GetStub().PutState(auctionID, newAuctionBytes) + if err != nil { + return fmt.Errorf("failed to update auction: %v", err) + } + + return nil +} + +// RevealBid is used by a bidder to reveal their bid after the auction is closed +func (s *SmartContract) RevealBid(ctx contractapi.TransactionContextInterface, auctionID string, txID string) error { + + // get bid from transient map + transientMap, err := ctx.GetStub().GetTransient() + if err != nil { + return fmt.Errorf("error getting transient: %v", err) + } + + transientBidJSON, ok := transientMap["bid"] + if !ok { + return fmt.Errorf("bid key not found in the transient map") + } + + // get implicit collection name of organization ID + collection, err := getCollectionName(ctx) + if err != nil { + return fmt.Errorf("failed to get implicit collection name: %v", err) + } + + // use transaction ID to create composit bid key + bidKey, err := ctx.GetStub().CreateCompositeKey(bidKeyType, []string{auctionID, txID}) + if err != nil { + return fmt.Errorf("failed to create composite key: %v", err) + } + + // get bid hash of bid if private bid on the public ledger + bidHash, err := ctx.GetStub().GetPrivateDataHash(collection, bidKey) + if err != nil { + return fmt.Errorf("failed to read bid bash from collection: %v", err) + } + if bidHash == nil { + return fmt.Errorf("bid hash does not exist: %s", bidKey) + } + + // get auction from public state + auctionBytes, err := ctx.GetStub().GetState(auctionID) + if err != nil { + return fmt.Errorf("failed to get auction %v: %v", auctionID, err) + } + if auctionBytes == nil { + return fmt.Errorf("Auction interest object %v not found", auctionID) + } + + var auctionJSON Auction + err = json.Unmarshal(auctionBytes, &auctionJSON) + if err != nil { + return fmt.Errorf("failed to create auction object JSON: %v", err) + } + + // Complete a series of three checks before we add the bid to the auction + + // check 1: check that the auction is closed. We cannot reveal an + // bid to an open auction + Status := auctionJSON.Status + if Status != "closed" { + return fmt.Errorf("cannot reveal bid for open or ended auction") + } + + // check 2: check that hash of revealed bid matches hash of private bid + // on the public ledger. This checks that the bidder is telling the truth + // about the value of their bid + + hash := sha256.New() + hash.Write(transientBidJSON) + calculatedBidJSONHash := hash.Sum(nil) + + // verify that the hash of the passed immutable properties matches the on-chain hash + if !bytes.Equal(calculatedBidJSONHash, bidHash) { + return fmt.Errorf("hash %x for bid JSON %s does not match hash in auction: %x", + calculatedBidJSONHash, + transientBidJSON, + bidHash, + ) + } + + // check 3; check hash of relealed bid matches hash of private bid that was + // added earlier. This ensures that the bid has not changed since it + // was added to the auction + + bidders := auctionJSON.PrivateBids + privateBidHashString := bidders[bidKey].Hash + + onChainBidHashString := fmt.Sprintf("%x", bidHash) + if privateBidHashString != onChainBidHashString { + return fmt.Errorf("hash %s for bid JSON %s does not match hash in auction: %s, bidder must have changed bid", + privateBidHashString, + transientBidJSON, + onChainBidHashString, + ) + } + + // we can add the bid to the auction if all checks have passed + type transientBidInput struct { + Quantity int `json:"quantity"` + Price int `json:"price"` + Org string `json:"org"` + Buyer string `json:"buyer"` + } + + // unmarshal bid imput + var bidInput transientBidInput + err = json.Unmarshal(transientBidJSON, &bidInput) + if err != nil { + return fmt.Errorf("failed to unmarshal JSON: %v", err) + } + + // Get ID of submitting client identity + clientID, err := ctx.GetClientIdentity().GetID() + if err != nil { + return fmt.Errorf("failed to get client identity %v", err) + } + + // marshal transient parameters and ID and MSPID into bid object + NewBid := FullBid{ + Type: bidKeyType, + Quantity: bidInput.Quantity, + Price: bidInput.Price, + Org: bidInput.Org, + Buyer: bidInput.Buyer, + } + + // check 4: make sure that the transaction is being submitted is the bidder + if bidInput.Buyer != clientID { + return fmt.Errorf("Permission denied, client id %v is not the owner of the bid", clientID) + } + + revealedBids := make(map[string]FullBid) + revealedBids = auctionJSON.RevealedBids + revealedBids[bidKey] = NewBid + auctionJSON.RevealedBids = revealedBids + + newAuctionBytes, _ := json.Marshal(auctionJSON) + + // put auction with bid added back into state + err = ctx.GetStub().PutState(auctionID, newAuctionBytes) + if err != nil { + return fmt.Errorf("failed to update auction: %v", err) + } + + return nil +} + +// CloseAuction can be used by the seller to close the auction. This prevents +// bids from being added to the auction, and allows users to reveal their bid +func (s *SmartContract) CloseAuction(ctx contractapi.TransactionContextInterface, auctionID string) error { + + auctionBytes, err := ctx.GetStub().GetState(auctionID) + if err != nil { + return fmt.Errorf("failed to get auction %v: %v", auctionID, err) + } + + if auctionBytes == nil { + return fmt.Errorf("Auction interest object %v not found", auctionID) + } + + var auctionJSON Auction + err = json.Unmarshal(auctionBytes, &auctionJSON) + if err != nil { + return fmt.Errorf("failed to create auction object JSON: %v", err) + } + + // the auction can only be closed by the seller + + // get ID of submitting client + clientID, err := ctx.GetClientIdentity().GetID() + if err != nil { + return fmt.Errorf("failed to get client identity %v", err) + } + + Seller := auctionJSON.Seller + if Seller != clientID { + return fmt.Errorf("auction can only be closed by seller: %v", err) + } + + Status := auctionJSON.Status + if Status != "open" { + return fmt.Errorf("cannot close auction that is not open") + } + + auctionJSON.Status = string("closed") + + closedAuction, _ := json.Marshal(auctionJSON) + + err = ctx.GetStub().PutState(auctionID, closedAuction) + if err != nil { + return fmt.Errorf("failed to close auction: %v", err) + } + + return nil +} + +// EndAuction both changes the auction status to closed and calculates the winners +// of the auction +func (s *SmartContract) EndAuction(ctx contractapi.TransactionContextInterface, auctionID string) error { + + auctionBytes, err := ctx.GetStub().GetState(auctionID) + if err != nil { + return fmt.Errorf("failed to get auction %v: %v", auctionID, err) + } + + if auctionBytes == nil { + return fmt.Errorf("Auction interest object %v not found", auctionID) + } + + var auctionJSON Auction + err = json.Unmarshal(auctionBytes, &auctionJSON) + if err != nil { + return fmt.Errorf("failed to create auction object JSON: %v", err) + } + + // Check that the auction is being ended by the seller + + // get ID of submitting client + clientID, err := ctx.GetClientIdentity().GetID() + if err != nil { + return fmt.Errorf("failed to get client identity %v", err) + } + + Seller := auctionJSON.Seller + if Seller != clientID { + return fmt.Errorf("auction can only be ended by seller: %v", err) + } + + Status := auctionJSON.Status + if Status != "closed" { + return fmt.Errorf("Can only end a closed auction") + } + + // get the list of revealed bids + + revealedBidMap := auctionJSON.RevealedBids + if len(auctionJSON.RevealedBids) == 0 { + return fmt.Errorf("No bids have been revealed, cannot end auction: %v", err) + } + + // sort the map of revealed bids to make it easier to calculate winners + // if bids are tied, fill smaller bids first + var Bidders []FullBid + + for _, bid := range revealedBidMap { + Bidders = append(Bidders, bid) + } + + sort.Slice(Bidders, func(p, q int) bool { + if Bidders[p].Price > Bidders[q].Price { + return true + } + if Bidders[p].Price < Bidders[q].Price { + return false + } + return Bidders[p].Quantity < Bidders[q].Quantity + }) + + i := 0 + remainingQuantity := auctionJSON.Quantity + + // calculate the winners + for remainingQuantity > 0 { + + // create the next winning bid + winner := Winners{ + Buyer: Bidders[i].Buyer, + Quantity: Bidders[i].Quantity, + } + + // add them to the list of winners and change the winning price + auctionJSON.Winners = append(auctionJSON.Winners, winner) + auctionJSON.Price = Bidders[i].Price + + // Calculate the quantity that goes to the winner + // if there is sufficient quantity to give them the full bid + if remainingQuantity > Bidders[i].Quantity { + remainingQuantity = remainingQuantity - Bidders[i].Quantity + + // if there is not, give the remainder + } else { + auctionJSON.Winners[i].Quantity = remainingQuantity + remainingQuantity = 0 + } + i++ + } + + // check if there is a winning bid that has yet to be revealed + err = queryAllBids(ctx, auctionJSON.Price, auctionJSON.RevealedBids, auctionJSON.PrivateBids) + if err != nil { + return fmt.Errorf("Cannot close auction: %v", err) + } + + auctionJSON.Status = string("ended") + + closedAuction, _ := json.Marshal(auctionJSON) + + err = ctx.GetStub().PutState(auctionID, closedAuction) + if err != nil { + return fmt.Errorf("failed to close auction: %v", err) + } + return nil +} diff --git a/auction/chaincode-go/smart-contract/auctionQueries.go b/auction/chaincode-go/smart-contract/auctionQueries.go new file mode 100644 index 00000000..6ba09f28 --- /dev/null +++ b/auction/chaincode-go/smart-contract/auctionQueries.go @@ -0,0 +1,148 @@ +/* +SPDX-License-Identifier: Apache-2.0 +*/ + +package auction + +import ( + "encoding/json" + "fmt" + + "github.com/hyperledger/fabric-chaincode-go/shim" + "github.com/hyperledger/fabric-contract-api-go/contractapi" +) + +// QueryAuction allows all members of the channel to read a public auction +func (s *SmartContract) QueryAuction(ctx contractapi.TransactionContextInterface, auctionID string) (*Auction, error) { + + auctionJSON, err := ctx.GetStub().GetState(auctionID) + if err != nil { + return nil, fmt.Errorf("failed to get auction object %v: %v", auctionID, err) + } + if auctionJSON == nil { + return nil, fmt.Errorf("auction does not exist") + } + + var auction *Auction + err = json.Unmarshal(auctionJSON, &auction) + if err != nil { + return nil, err + } + + return auction, nil +} + +// QueryBid allows the submitter of the bid to read their bid from public state +func (s *SmartContract) QueryBid(ctx contractapi.TransactionContextInterface, auctionID string, txID string) (*FullBid, error) { + + err := verifyClientOrgMatchesPeerOrg(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get implicit collection name: %v", err) + } + + clientID, err := ctx.GetClientIdentity().GetID() + if err != nil { + return nil, fmt.Errorf("failed to get client identity %v", err) + } + + collection, err := getCollectionName(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get implicit collection name: %v", err) + } + + bidKey, err := ctx.GetStub().CreateCompositeKey(bidKeyType, []string{auctionID, txID}) + if err != nil { + return nil, fmt.Errorf("failed to create composite key: %v", err) + } + + bidJSON, err := ctx.GetStub().GetPrivateData(collection, bidKey) + if err != nil { + return nil, fmt.Errorf("failed to get bid %v: %v", bidKey, err) + } + if bidJSON == nil { + return nil, fmt.Errorf("bid %v does not exist", bidKey) + } + + var bid *FullBid + err = json.Unmarshal(bidJSON, &bid) + if err != nil { + return nil, err + } + + // check that the client querying the bid is the bid owner + if bid.Buyer != clientID { + return nil, fmt.Errorf("Permission denied, client id %v is not the owner of the bid", clientID) + } + + return bid, nil +} + +// GetID is an internal helper function to allow users to get their identity +func (s *SmartContract) GetID(ctx contractapi.TransactionContextInterface) (string, error) { + + // Get the MSP ID of submitting client identity + clientID, err := ctx.GetClientIdentity().GetID() + if err != nil { + return "", fmt.Errorf("failed to get verified MSPID: %v", err) + } + + return clientID, nil +} + +// queryAllBids is an internal function that is used to determine if a winning bid has yet to be revealed +func queryAllBids(ctx contractapi.TransactionContextInterface, auctionPrice int, revealedBidders map[string]FullBid, bidders map[string]BidHash) error { + + // Get MSP ID of peer org + peerMSPID, err := shim.GetMSPID() + if err != nil { + return fmt.Errorf("failed getting the peer's MSPID: %v", err) + } + + var error error + error = nil + + for bidKey, privateBid := range bidders { + + if _, bidInAuction := revealedBidders[bidKey]; bidInAuction { + + //bid is already revealed, no action to take + + } else { + + collection := "_implicit_org_" + privateBid.Org + + if privateBid.Org == peerMSPID { + + bidJSON, err := ctx.GetStub().GetPrivateData(collection, bidKey) + if err != nil { + return fmt.Errorf("failed to get bid %v: %v", bidKey, err) + } + if bidJSON == nil { + return fmt.Errorf("bid %v does not exist", bidKey) + } + + var bid *FullBid + err = json.Unmarshal(bidJSON, &bid) + if err != nil { + return err + } + + if bid.Price > auctionPrice { + error = fmt.Errorf("Cannot close auction, bidder has a higher price: %v", err) + } + + } else { + + Hash, err := ctx.GetStub().GetPrivateDataHash(collection, bidKey) + if err != nil { + return fmt.Errorf("failed to read bid hash from collection: %v", err) + } + if Hash == nil { + return fmt.Errorf("bid hash does not exist: %s", bidKey) + } + } + } + } + + return error +} diff --git a/auction/chaincode-go/smart-contract/utils.go b/auction/chaincode-go/smart-contract/utils.go new file mode 100644 index 00000000..8c305246 --- /dev/null +++ b/auction/chaincode-go/smart-contract/utils.go @@ -0,0 +1,107 @@ +/* +SPDX-License-Identifier: Apache-2.0 +*/ + +package auction + +import ( + "fmt" + + "github.com/hyperledger/fabric-chaincode-go/pkg/statebased" + "github.com/hyperledger/fabric-chaincode-go/shim" + "github.com/hyperledger/fabric-contract-api-go/contractapi" +) + +// setAssetStateBasedEndorsement sets the endorsement policy of a new auction +func setAssetStateBasedEndorsement(ctx contractapi.TransactionContextInterface, auctionID string, orgToEndorse string) error { + + endorsementPolicy, err := statebased.NewStateEP(nil) + if err != nil { + return err + } + err = endorsementPolicy.AddOrgs(statebased.RoleTypePeer, orgToEndorse) + if err != nil { + return fmt.Errorf("failed to add org to endorsement policy: %v", err) + } + policy, err := endorsementPolicy.Policy() + if err != nil { + return fmt.Errorf("failed to create endorsement policy bytes from org: %v", err) + } + err = ctx.GetStub().SetStateValidationParameter(auctionID, policy) + if err != nil { + return fmt.Errorf("failed to set validation parameter on auction: %v", err) + } + + return nil +} + +// addAssetStateBasedEndorsement adds a new organization as an endorser of the auction +func addAssetStateBasedEndorsement(ctx contractapi.TransactionContextInterface, auctionID string, orgToEndorse string) error { + + endorsementPolicy, err := ctx.GetStub().GetStateValidationParameter(auctionID) + if err != nil { + return err + } + + newEndorsementPolicy, err := statebased.NewStateEP(endorsementPolicy) + if err != nil { + return err + } + + err = newEndorsementPolicy.AddOrgs(statebased.RoleTypePeer, orgToEndorse) + if err != nil { + return fmt.Errorf("failed to add org to endorsement policy: %v", err) + } + policy, err := newEndorsementPolicy.Policy() + if err != nil { + return fmt.Errorf("failed to create endorsement policy bytes from org: %v", err) + } + err = ctx.GetStub().SetStateValidationParameter(auctionID, policy) + if err != nil { + return fmt.Errorf("failed to set validation parameter on auction: %v", err) + } + + return nil +} + +// getCollectionName is an internal helper function to get collection of submitting client identity. +func getCollectionName(ctx contractapi.TransactionContextInterface) (string, error) { + + // Get the MSP ID of submitting client identity + clientMSPID, err := ctx.GetClientIdentity().GetMSPID() + if err != nil { + return "", fmt.Errorf("failed to get verified MSPID: %v", err) + } + + // Create the collection name + orgCollection := "_implicit_org_" + clientMSPID + + return orgCollection, nil +} + +// verifyClientOrgMatchesPeerOrg is an internal function used to verify that client org id matches peer org id. +func verifyClientOrgMatchesPeerOrg(ctx contractapi.TransactionContextInterface) error { + clientMSPID, err := ctx.GetClientIdentity().GetMSPID() + if err != nil { + return fmt.Errorf("failed getting the client's MSPID: %v", err) + } + peerMSPID, err := shim.GetMSPID() + if err != nil { + return fmt.Errorf("failed getting the peer's MSPID: %v", err) + } + + if clientMSPID != peerMSPID { + return fmt.Errorf("client from org %v is not authorized to read or write private data from an org %v peer", clientMSPID, peerMSPID) + } + + return nil +} + +func contains(sli []string, str string) bool { + for _, a := range sli { + if a == str { + return true + } + } + return false +} diff --git a/auction/chaincode-go/smartContract.go b/auction/chaincode-go/smartContract.go new file mode 100644 index 00000000..8aa5637b --- /dev/null +++ b/auction/chaincode-go/smartContract.go @@ -0,0 +1,23 @@ +/* +SPDX-License-Identifier: Apache-2.0 +*/ + +package main + +import ( + "log" + + "github.com/hyperledger/fabric-contract-api-go/contractapi" + "github.com/hyperledger/fabric-samples/auction/chaincode-go/smart-contract" +) + +func main() { + auctionSmartContract, err := contractapi.NewChaincode(&auction.SmartContract{}) + if err != nil { + log.Panicf("Error creating auction chaincode: %v", err) + } + + if err := auctionSmartContract.Start(); err != nil { + log.Panicf("Error starting auction chaincode: %v", err) + } +} diff --git a/auction/dutch-auction/README.md b/auction/dutch-auction/README.md new file mode 100644 index 00000000..cfe45054 --- /dev/null +++ b/auction/dutch-auction/README.md @@ -0,0 +1,431 @@ +## Running a Dutch auction + +The auction sample smart contract can be used to implement a [Dutch auction](https://en.wikipedia.org/wiki/Dutch_auction) to sell multiple items of the same good. All items are sold at the price that clears the auction. This tutorial creates an auction to sell 100 tickets to multiple bidders. + +## Deploy the chaincode + +Change into the test network directory. +``` +cd fabric-samples/test-network +``` + +If the test network is already running, run the following command to bring the network down and start from a clean initial state. +``` +./network.sh down +``` + +You can then run the following command to deploy a new network. +``` +./network.sh up createChannel -ca +``` + +Run the following command to deploy the auction smart contract. +``` +./network.sh deployCC -ccn auction -ccp ../auction/chaincode-go/ -ccep "OR('Org1MSP.peer','Org2MSP.peer')" +``` + +## Install the application dependencies + +Change into the `application-javascript` directory: +``` +cd fabric-samples/auction/application-javascript +``` + +From this directory, run the following command to download the application dependencies if you have not done so already: +``` +npm install +``` + +## Register and enroll the application identities + +To interact with the network, you will need to enroll the Certificate Authority administrators of Org1 and Org2. You can use the `enrollAdmin.js` program for this task. Run the following command to enroll the Org1 admin: +``` +node enrollAdmin.js org1 +``` +You should see the logs of the admin wallet being created on your local file system. Now run the command to enroll the CA admin of Org2: +``` +node enrollAdmin.js org2 +``` + +We can use the CA admins of both organizations to register and enroll the identities of the seller that will create the auction and the bidders who will try to purchase the tickets. + +Run the following command to register and enroll the seller identity that will create the auction. The seller will belong to Org1. +``` +node registerEnrollUser.js org1 seller +``` + +You should see the logs of the seller wallet being created as well. Run the following commands to register and enroll 2 bidders from Org1 and another 3 bidders from Org2: +``` +node registerEnrollUser.js org1 bidder1 +node registerEnrollUser.js org1 bidder2 +node registerEnrollUser.js org2 bidder3 +node registerEnrollUser.js org2 bidder4 +node registerEnrollUser.js org2 bidder5 +``` + +## Create the auction + +The seller from Org1 would like to create an auction to sell 100 tickets. Run the following command to use the seller wallet to run the `createAuction.js` application. The seller needs to provide an ID for the auction, the item to be sold, and the quantity to be sold to create the auction: +``` +node createAuction.js org1 seller auction1 tickets 100 +``` + +You will see the application query the auction after it is created. + +## Bid on the auction + +We can now use the bidder wallets to submit bids to the auction: + +### Bid as bidder1 + +Bidder1 will create a bid to purchase 50 tickets for 80 dollars. +``` +node bid.js org1 bidder1 auction1 50 80 +``` + +The application will query the bid after it is created: +``` +*** Result: Bid: { + "objectType": "bid", + "quantity": 50, + "price": 80, + "org": "Org1MSP", + "buyer": "eDUwOTo6Q049YmlkZGVyMSxPVT1jbGllbnQrT1U9b3JnMStPVT1kZXBhcnRtZW50MTo6Q049Y2Eub3JnMS5leGFtcGxlLmNvbSxPPW9yZzEuZXhhbXBsZS5jb20sTD1EdXJoYW0sU1Q9Tm9ydGggQ2Fyb2xpbmEsQz1VUw==" +} +``` + +The `bid.js` application also prints the bidID: +``` +*** Result ***SAVE THIS VALUE*** BidID: 61e9b0fc1913f10872625bea4a6555522c70070416209848cc1d8fb6101133ad +``` + +The BidID acts as the unique identifier for the bid. This ID allows you to query the bid using the `queryBid.js` program and add the bid to the auction. Save the bidID returned by the application as an environment variable in your terminal: +``` +export BIDDER1_BID_ID=61e9b0fc1913f10872625bea4a6555522c70070416209848cc1d8fb6101133ad +``` +This value will be different for each transaction, so you will need to use the value returned in your terminal. + +Now that the bid has been created, you can submit the bid to the auction. Run the following command to submit the bid that was just created: +``` +node submitBid.js org1 bidder1 auction1 $BIDDER1_BID_ID +``` + +The hash of bid is added to the list of private bids in that have been submitted to `auction1`. Storing the hash on the public auction ledger allows users to prove the accuracy of the bids they reveal once bidding is closed. The application queries the auction to verify that the bid was added: +``` +*** Result: Auction: { + "objectType": "auction", + "item": "tickets", + "seller": "eDUwOTo6Q049c2VsbGVyLE9VPWNsaWVudCtPVT1vcmcxK09VPWRlcGFydG1lbnQxOjpDTj1jYS5vcmcxLmV4YW1wbGUuY29tLE89b3JnMS5leGFtcGxlLmNvbSxMPUR1cmhhbSxTVD1Ob3J0aCBDYXJvbGluYSxDPVVT", + "quantity": 100, + "organizations": [ + "Org1MSP" + ], + "privateBids": { + "\u0000bid\u0000auction1\u000061e9b0fc1913f10872625bea4a6555522c70070416209848cc1d8fb6101133ad\u0000": { + "org": "Org1MSP", + "hash": "584dbad2269a44afb42bdbc7a7a4c08a7cd50deece1eb3fc38d4e49b9342f270" + } + }, + "revealedBids": {}, + "winners": [], + "price": 0, + "status": "open" +} +``` + +### Bid as bidder2 + +Let's submit another bid. Bidder2 would like to purchase 40 tickets for 50 dollars. +``` +node bid.js org1 bidder2 auction1 40 50 +``` + +Save the Bid ID returned by the application: +``` +export BIDDER2_BID_ID=911c7920a7ba4643a531cb2d5d274d303fdb2e6800f50aeb6b725af0b7162ea2 +``` + +Submit bidder2's bid to the auction: +``` +node submitBid.js org1 bidder2 auction1 $BIDDER2_BID_ID +``` + +### Bid as bidder3 from Org2 + +Bidder3 will bid for 30 tickets at 70 dollars: +``` +node bid.js org2 bidder3 auction1 30 70 +``` + +Save the Bid ID returned by the application: +``` +export BIDDER3_BID_ID=93a8164628fa28290554b5dc6f505cbb8c7498d8f7c60f7df33d4a1cffb8fa47 +``` + +Add bidder3's bid to the auction: +``` +node submitBid.js org2 bidder3 auction1 $BIDDER3_BID_ID +``` + +Because bidder3 belongs to Org2, submitting the bid will add Org2 to the list of participating organizations. You can see the Org2 MSP ID has been added to the list of `"organizations"` in the updated auction returned by the application: +``` +*** Result: Auction: { + "objectType": "auction", + "item": "tickets", + "seller": "eDUwOTo6Q049c2VsbGVyLE9VPWNsaWVudCtPVT1vcmcxK09VPWRlcGFydG1lbnQxOjpDTj1jYS5vcmcxLmV4YW1wbGUuY29tLE89b3JnMS5leGFtcGxlLmNvbSxMPUR1cmhhbSxTVD1Ob3J0aCBDYXJvbGluYSxDPVVT", + "quantity": 100, + "organizations": [ + "Org1MSP", + "Org2MSP" + ], + "privateBids": { + "\u0000bid\u0000auction1\u000061e9b0fc1913f10872625bea4a6555522c70070416209848cc1d8fb6101133ad\u0000": { + "org": "Org1MSP", + "hash": "584dbad2269a44afb42bdbc7a7a4c08a7cd50deece1eb3fc38d4e49b9342f270" + }, + "\u0000bid\u0000auction1\u0000911c7920a7ba4643a531cb2d5d274d303fdb2e6800f50aeb6b725af0b7162ea2\u0000": { + "org": "Org1MSP", + "hash": "bbcd0c7c376e6681a76d8c5482c97f8bdcda55c90c5478100c3aef17815c4fd3" + }, + "\u0000bid\u0000auction1\u000093a8164628fa28290554b5dc6f505cbb8c7498d8f7c60f7df33d4a1cffb8fa47\u0000": { + "org": "Org2MSP", + "hash": "4446c7eb0e2d64165a916ee996348a18716f4c97e632d58d5a8c20eeec5a9238" + } + }, + "revealedBids": {}, + "winners": [], + "price": 0, + "status": "open" +} +``` + +Now that a bid from Org2 has been added to the auction, any updates to the auction need to be endorsed by the Org2 peer. The applications will use the `"organizations"` field to specify which organizations need to endorse submitting a new bid, revealing a bid, or updating the auction status. + +### Bid as bidder4 + +Bidder4 from Org2 would like to purchase 15 tickets for 60 dollars: +``` +node bid.js org2 bidder4 auction1 15 60 +``` + +Save the Bid ID returned by the application: +``` +export BIDDER4_BID_ID=324de04c459c5a38f103e9096dea06e19faca88f84dd5175ba0e6fd6a9d7d140 +``` + +Add bidder4's bid to the auction: +``` +node submitBid.js org2 bidder4 auction1 $BIDDER4_BID_ID +``` + +### Bid as bidder5 + +Bidder5 from Org2 will bid for 20 tickets at 60 dollars: +``` +node bid.js org2 bidder4 auction1 20 60 +``` + +Save the Bid ID returned by the application: +``` +export BIDDER5_BID_ID=3245049bd81a8cecfbc006f29eed1df0570e385e63daedd19c06b2c92d2067ae +``` + +Add bidder4's bid to the auction: +``` +node submitBid.js org2 bidder5 auction1 $BIDDER5_BID_ID +``` + + +## Close the auction + +Now that all five bidders have joined the auction, the seller would like to close the auction and allow buyers to reveal their bids. The seller identity that created the auction needs to submit the transaction: +``` +node closeAuction.js org1 seller auction1 +``` + +The application will query the auction to allow you to verify that the auction status has changed to closed. + +## Reveal bids + +After the auction is closed, bidders can try to win the auction by revealing their bids. The transaction to reveal a bid needs to pass four checks: +1. The auction is closed. +2. The transaction was submitted by the identity that created the bid. +3. The hash of the revealed bid matches the hash of the bid on the channel ledger. This confirms that the bid is the same as the bid that is stored in the private data collection. +4. The hash of the revealed bid matches the hash that was submitted to the auction. This confirms that the bid was not altered after the auction was closed. + +Use the `revealBid.js` application to reveal the bid of Bidder1: +``` +node revealBid.js org1 bidder1 auction1 $BIDDER1_BID_ID +``` + +The full bid details, including the quantity and price, are now visible: +``` +*** Result: Auction: { + "objectType": "auction", + "item": "tickets", + "seller": "eDUwOTo6Q049c2VsbGVyLE9VPWNsaWVudCtPVT1vcmcxK09VPWRlcGFydG1lbnQxOjpDTj1jYS5vcmcxLmV4YW1wbGUuY29tLE89b3JnMS5leGFtcGxlLmNvbSxMPUR1cmhhbSxTVD1Ob3J0aCBDYXJvbGluYSxDPVVT", + "quantity": 100, + "organizations": [ + "Org1MSP", + "Org2MSP" + ], + "privateBids": { + "\u0000bid\u0000auction1\u00003245049bd81a8cecfbc006f29eed1df0570e385e63daedd19c06b2c92d2067ae\u0000": { + "org": "Org2MSP", + "hash": "5a51070f188e6480fe606acdc5ad1a1c36330adc3547eb021aaf87aa00ec79f7" + }, + "\u0000bid\u0000auction1\u0000324de04c459c5a38f103e9096dea06e19faca88f84dd5175ba0e6fd6a9d7d140\u0000": { + "org": "Org2MSP", + "hash": "516c775f7da2fd653dab71d20d60a6e8bea8ff856803af4b59f943ca2ba40699" + }, + "\u0000bid\u0000auction1\u000061e9b0fc1913f10872625bea4a6555522c70070416209848cc1d8fb6101133ad\u0000": { + "org": "Org1MSP", + "hash": "584dbad2269a44afb42bdbc7a7a4c08a7cd50deece1eb3fc38d4e49b9342f270" + }, + "\u0000bid\u0000auction1\u0000911c7920a7ba4643a531cb2d5d274d303fdb2e6800f50aeb6b725af0b7162ea2\u0000": { + "org": "Org1MSP", + "hash": "bbcd0c7c376e6681a76d8c5482c97f8bdcda55c90c5478100c3aef17815c4fd3" + }, + "\u0000bid\u0000auction1\u000093a8164628fa28290554b5dc6f505cbb8c7498d8f7c60f7df33d4a1cffb8fa47\u0000": { + "org": "Org2MSP", + "hash": "4446c7eb0e2d64165a916ee996348a18716f4c97e632d58d5a8c20eeec5a9238" + } + }, + "revealedBids": { + "\u0000bid\u0000auction1\u000061e9b0fc1913f10872625bea4a6555522c70070416209848cc1d8fb6101133ad\u0000": { + "objectType": "bid", + "quantity": 50, + "price": 80, + "org": "Org1MSP", + "buyer": "eDUwOTo6Q049YmlkZGVyMSxPVT1jbGllbnQrT1U9b3JnMStPVT1kZXBhcnRtZW50MTo6Q049Y2Eub3JnMS5leGFtcGxlLmNvbSxPPW9yZzEuZXhhbXBsZS5jb20sTD1EdXJoYW0sU1Q9Tm9ydGggQ2Fyb2xpbmEsQz1VUw==" + } + }, + "winners": [], + "price": 0, + "status": "closed" +} +``` + +All bidders will reveal their bid to participate in the auction. Run the following commands to reveal the bids of the remaining four bidders: +``` +node revealBid.js org1 bidder2 auction1 $BIDDER2_BID_ID +node revealBid.js org2 bidder3 auction1 $BIDDER3_BID_ID +node revealBid.js org2 bidder4 auction1 $BIDDER4_BID_ID +node revealBid.js org2 bidder5 auction1 $BIDDER5_BID_ID +``` + +## End the auction + +Now that the winning bids have been revealed, we can end the auction: +``` +node endAuction org1 seller auction1 +``` + +The transaction was successfully endorsed by both Org1 and Org2, who both calculated the same price and winners of the auction. Each winning bidder is listed next to the quantity that was allocated to them. + +``` +*** Result: Auction: { + "objectType": "auction", + "item": "tickets", + "seller": "eDUwOTo6Q049c2VsbGVyLE9VPWNsaWVudCtPVT1vcmcxK09VPWRlcGFydG1lbnQxOjpDTj1jYS5vcmcxLmV4YW1wbGUuY29tLE89b3JnMS5leGFtcGxlLmNvbSxMPUR1cmhhbSxTVD1Ob3J0aCBDYXJvbGluYSxDPVVT", + "quantity": 100, + "organizations": [ + "Org1MSP", + "Org2MSP" + ], + "privateBids": { + "\u0000bid\u0000auction1\u00003245049bd81a8cecfbc006f29eed1df0570e385e63daedd19c06b2c92d2067ae\u0000": { + "org": "Org2MSP", + "hash": "5a51070f188e6480fe606acdc5ad1a1c36330adc3547eb021aaf87aa00ec79f7" + }, + "\u0000bid\u0000auction1\u0000324de04c459c5a38f103e9096dea06e19faca88f84dd5175ba0e6fd6a9d7d140\u0000": { + "org": "Org2MSP", + "hash": "516c775f7da2fd653dab71d20d60a6e8bea8ff856803af4b59f943ca2ba40699" + }, + "\u0000bid\u0000auction1\u000061e9b0fc1913f10872625bea4a6555522c70070416209848cc1d8fb6101133ad\u0000": { + "org": "Org1MSP", + "hash": "584dbad2269a44afb42bdbc7a7a4c08a7cd50deece1eb3fc38d4e49b9342f270" + }, + "\u0000bid\u0000auction1\u0000911c7920a7ba4643a531cb2d5d274d303fdb2e6800f50aeb6b725af0b7162ea2\u0000": { + "org": "Org1MSP", + "hash": "bbcd0c7c376e6681a76d8c5482c97f8bdcda55c90c5478100c3aef17815c4fd3" + }, + "\u0000bid\u0000auction1\u000093a8164628fa28290554b5dc6f505cbb8c7498d8f7c60f7df33d4a1cffb8fa47\u0000": { + "org": "Org2MSP", + "hash": "4446c7eb0e2d64165a916ee996348a18716f4c97e632d58d5a8c20eeec5a9238" + } + }, + "revealedBids": { + "\u0000bid\u0000auction1\u00003245049bd81a8cecfbc006f29eed1df0570e385e63daedd19c06b2c92d2067ae\u0000": { + "objectType": "bid", + "quantity": 20, + "price": 60, + "org": "Org2MSP", + "buyer": "eDUwOTo6Q049YmlkZGVyNCxPVT1jbGllbnQrT1U9b3JnMitPVT1kZXBhcnRtZW50MTo6Q049Y2Eub3JnMi5leGFtcGxlLmNvbSxPPW9yZzIuZXhhbXBsZS5jb20sTD1IdXJzbGV5LFNUPUhhbXBzaGlyZSxDPVVL" + }, + "\u0000bid\u0000auction1\u0000324de04c459c5a38f103e9096dea06e19faca88f84dd5175ba0e6fd6a9d7d140\u0000": { + "objectType": "bid", + "quantity": 15, + "price": 60, + "org": "Org2MSP", + "buyer": "eDUwOTo6Q049YmlkZGVyNCxPVT1jbGllbnQrT1U9b3JnMitPVT1kZXBhcnRtZW50MTo6Q049Y2Eub3JnMi5leGFtcGxlLmNvbSxPPW9yZzIuZXhhbXBsZS5jb20sTD1IdXJzbGV5LFNUPUhhbXBzaGlyZSxDPVVL" + }, + "\u0000bid\u0000auction1\u000061e9b0fc1913f10872625bea4a6555522c70070416209848cc1d8fb6101133ad\u0000": { + "objectType": "bid", + "quantity": 50, + "price": 80, + "org": "Org1MSP", + "buyer": "eDUwOTo6Q049YmlkZGVyMSxPVT1jbGllbnQrT1U9b3JnMStPVT1kZXBhcnRtZW50MTo6Q049Y2Eub3JnMS5leGFtcGxlLmNvbSxPPW9yZzEuZXhhbXBsZS5jb20sTD1EdXJoYW0sU1Q9Tm9ydGggQ2Fyb2xpbmEsQz1VUw==" + }, + "\u0000bid\u0000auction1\u0000911c7920a7ba4643a531cb2d5d274d303fdb2e6800f50aeb6b725af0b7162ea2\u0000": { + "objectType": "bid", + "quantity": 40, + "price": 50, + "org": "Org1MSP", + "buyer": "eDUwOTo6Q049YmlkZGVyMixPVT1jbGllbnQrT1U9b3JnMStPVT1kZXBhcnRtZW50MTo6Q049Y2Eub3JnMS5leGFtcGxlLmNvbSxPPW9yZzEuZXhhbXBsZS5jb20sTD1EdXJoYW0sU1Q9Tm9ydGggQ2Fyb2xpbmEsQz1VUw==" + }, + "\u0000bid\u0000auction1\u000093a8164628fa28290554b5dc6f505cbb8c7498d8f7c60f7df33d4a1cffb8fa47\u0000": { + "objectType": "bid", + "quantity": 30, + "price": 70, + "org": "Org2MSP", + "buyer": "eDUwOTo6Q049YmlkZGVyMyxPVT1jbGllbnQrT1U9b3JnMitPVT1kZXBhcnRtZW50MTo6Q049Y2Eub3JnMi5leGFtcGxlLmNvbSxPPW9yZzIuZXhhbXBsZS5jb20sTD1IdXJzbGV5LFNUPUhhbXBzaGlyZSxDPVVL" + } + }, + "winners": [ + { + "buyer": "eDUwOTo6Q049YmlkZGVyMSxPVT1jbGllbnQrT1U9b3JnMStPVT1kZXBhcnRtZW50MTo6Q049Y2Eub3JnMS5leGFtcGxlLmNvbSxPPW9yZzEuZXhhbXBsZS5jb20sTD1EdXJoYW0sU1Q9Tm9ydGggQ2Fyb2xpbmEsQz1VUw==", + "quantity": 50 + }, + { + "buyer": "eDUwOTo6Q049YmlkZGVyMyxPVT1jbGllbnQrT1U9b3JnMitPVT1kZXBhcnRtZW50MTo6Q049Y2Eub3JnMi5leGFtcGxlLmNvbSxPPW9yZzIuZXhhbXBsZS5jb20sTD1IdXJzbGV5LFNUPUhhbXBzaGlyZSxDPVVL", + "quantity": 30 + }, + { + "buyer": "eDUwOTo6Q049YmlkZGVyNCxPVT1jbGllbnQrT1U9b3JnMitPVT1kZXBhcnRtZW50MTo6Q049Y2Eub3JnMi5leGFtcGxlLmNvbSxPPW9yZzIuZXhhbXBsZS5jb20sTD1IdXJzbGV5LFNUPUhhbXBzaGlyZSxDPVVL", + "quantity": 15 + }, + { + "buyer": "eDUwOTo6Q049YmlkZGVyNCxPVT1jbGllbnQrT1U9b3JnMitPVT1kZXBhcnRtZW50MTo6Q049Y2Eub3JnMi5leGFtcGxlLmNvbSxPPW9yZzIuZXhhbXBsZS5jb20sTD1IdXJzbGV5LFNUPUhhbXBzaGlyZSxDPVVL", + "quantity": 5 + } + ], + "price": 60, + "status": "ended" +} +``` + +The auction allocates tickets to the highest bids first. Because all 100 tickets are sold after allocating tickets to the bids that were submitted at 60, 60 is the `"price"` that clears the auction. The first 80 tickets are allocated to Bidder1 and Bidder3. The remaining 20 tickers are allocated to Bidder4 and Bidder5. When bids are tied, the auction smart contract fills the smaller bids first. As a result, Bidder4 is awarded their full bid of 15 tickets, while Bidder5 is allocated the remaining 5 tickets. + +## Clean up + +When your are done using the auction smart contract, you can bring down the network and clean up the environment. In the `auction/application-javascript` directory, run the following command to remove the wallets used to run the applications: +``` +rm -rf wallet +``` + +You can then navigate to the test network directory and bring down the network: +```` +cd ../../test-network/ +./network.sh down +```` From ff8a3c8d50ef72bb6390e2d34119678cf3b3203e Mon Sep 17 00:00:00 2001 From: Arnaud J Le Hors Date: Fri, 6 Nov 2020 19:08:34 +0100 Subject: [PATCH 33/38] Simplify auction sample (#367) This PR changes the auction sample to a simple blind auction with just a single item to be sold to the highest bidder. Signed-off-by: Arnaud J Le Hors --- auction/README.md | 201 ++++---- auction/application-javascript/bid.js | 24 +- .../application-javascript/createAuction.js | 16 +- auction/application-javascript/revealBid.js | 2 +- .../chaincode-go/smart-contract/auction.go | 82 +--- .../smart-contract/auctionQueries.go | 2 +- auction/dutch-auction/README.md | 431 ------------------ 7 files changed, 132 insertions(+), 626 deletions(-) delete mode 100644 auction/dutch-auction/README.md diff --git a/auction/README.md b/auction/README.md index 21e74397..1ab0cfc9 100644 --- a/auction/README.md +++ b/auction/README.md @@ -1,17 +1,17 @@ -## Auction sample +## Simple blind auction sample -The auction sample uses Hyperledger Fabric to run an auction where bids kept are private from other bidders. Instead of displaying the full bid on the public ledger, buyers can only see hashes of other bids while bidding is underway. This prevents potential buyers from changing their bids in response to bids that have already been submitted. After the bidding period ends, participants can reveal their bid to try to win the auction. The organizations participating in the auction verify that a revealed bid matches the hash on the public ledger. +The simple blind auction sample uses Hyperledger Fabric to run an auction where bids are kept private until the auction period is over. Instead of displaying the full bid on the public ledger, buyers can only see hashes of other bids while bidding is underway. This prevents buyers from changing their bids in response to bids submitted by others. After the bidding period ends, participants reveal their bid to try to win the auction. The organizations participating in the auction verify that a revealed bid matches the hash on the public ledger. Whichever has the highest bid wins. -A user that wants to sell one or more items can use the smart contract to create an auction. The auction is stored on the channel ledger and can be read by all channel members. The auctions created by the smart contract are run in three steps: -1. Each auction is created with the status **open**. While the auction is open, potential buyers can add new bids to the auction. The full bids of each buyer are stored in the implicit private data collection of their organization. After the bid is created, the bidder can submit the hash of the bid to the auction. A bid is added to the auction in two steps because the transaction that creates the bid only needs be endorsed by a peer of the bidder's organization, while a transaction that updates the auction may need to be endorsed by multiple organizations. When the bid is added to the auction, the bidder's organization is added to the list of organizations that need to endorse any updates to the auction. -2. The auction is **closed** to prevent additional bids from being added to the auction. After the auction is closed, the bidders that submitted bids to the auction can reveal their full bid. Only revealed bids can win the auction. -3. The auction is **ended** to calculate the winners from the set of revealed bids. All organizations participating in the auction calculate the price that clears the auction and the winning set of bids. The seller can end the auction only if all bidding organizations endorse the same winners and price. +A user that wants to sell one item can use the smart contract to create an auction. The auction is stored on the channel ledger and can be read by all channel members. The auctions created by the smart contract are run in three steps: +1. Each auction is created with the status **open**. While the auction is open, buyers can add new bids to the auction. The full bids of each buyer are stored in the implicit private data collections of their organization. After the bid is created, the bidder can submit the hash of the bid to the auction. A bid is added to the auction in two steps because the transaction that creates the bid only needs to be endorsed by a peer of the bidders organization, while a transaction that updates the auction may need to be endorsed by multiple organizations. When the bid is added to the auction, the bidder's organization is added to the list of organizations that need to endorse any updates to the auction. +2. The auction is **closed** to prevent additional bids from being added to the auction. After the auction is closed, bidders that submitted bids to the auction can reveal their full bid. Only revealed bids can win the auction. +3. The auction is **ended** to calculate the winner from the set of revealed bids. All organizations participating in the auction calculate the price that clears the auction and the winning bid. The seller can end the auction only if all bidding organizations endorse the same winner and price. - Before endorsing the transaction that ends the auction, each organization queries the implicit private data collection on their peers to check if any organization member has a winning bid that has not yet been revealed. If a winning bid is found, the organization will withhold their endorsement and prevent the auction from being closed. This prevents the seller from ending the auction prematurely, or colluding with buyers to end the auction at an artificially low price. +Before endorsing the transaction that ends the auction, each organization queries the implicit private data collection on their peers to check if any organization member has a winning bid that has not yet been revealed. If a winning bid is found, the organization will withhold their endorsement and prevent the auction from being closed. This prevents the seller from ending the auction prematurely, or colluding with buyers to end the auction at an artificially low price. -The sample uses several Fabric features to make the auction private and secure. Bids are stored in private data collections to prevent bids from being distributed to other peers in the channel. When bidding is closed, the auction smart contract uses the `GetPrivateDataHash()` API to verify that the bid stored in private data is the same bid as the one that is being revealed. State based endorsement is used to add the organization of each bidder to the auction endorsement policy. The smart contract uses the `GetClientIdentity.GetID()` API to ensure that only the potential buyer can read their bid from private state and only the seller can close or end the auction. +The sample uses several Fabric features to make the auction private and secure. Bids are stored in private data collections to prevent bids from being distributed to other peers in the channel. When bidding is closed, the auction smart contract uses the `GetPrivateDataHash()` API to verify that the bid stored in private data is the same bid that is being revealed. State based endorsement is used to add the organization of each bidder to the auction endorsement policy. The smart contract uses the `GetClientIdentity.GetID()` API to ensure that only the potential buyer can read their bid from private state and only the seller can close or end the auction. -This tutorial uses the auction smart contract in a scenario where one seller wants to sell a valuable painting. Four potential buyers from two different organizations will submit bids to purchase the painting. You can also use the auction smart contract implement a [Dutch auction](https://en.wikipedia.org/wiki/Dutch_auction) for multiple items of the same type. To run an auction that sells multiple items, see [Running a Dutch auction](dutch-auction). +This tutorial uses the auction smart contract in a scenario where one seller wants to auction a painting. Four potential buyers from two different organizations will submit bids to the auction and try to win the auction. ## Deploy the chaincode @@ -72,9 +72,9 @@ node registerEnrollUser.js org2 bidder4 ## Create the auction -The seller from Org1 would like to create an auction for the painting. Run the following command to use the seller wallet to run the `createAuction.js` application. The program will submit a transaction to the network that creates the auction on the channel ledger. The organization and identity name are passed to the application to use the wallet that was created by the `registerEnrollUser.js` application. The seller needs to provide an ID for the auction, the item to be sold, and the quantity to be sold to create the auction: +The seller from Org1 would like to create an auction to sell a vintage Matchbox painting. Run the following command to use the seller wallet to run the `createAuction.js` application. The program will submit a transaction to the network that creates the auction on the channel ledger. The organization and identity name are passed to the application to use the wallet that was created by the `registerEnrollUser.js` application. The seller needs to provide an ID for the auction and the item to be sold to create the auction: ``` -node createAuction.js org1 seller auction1 painting 1 +node createAuction.js org1 seller PaintingAuction painting ``` After the transaction is complete, the `createAuction.js` application will query the auction stored in the public channel ledger: @@ -83,13 +83,12 @@ After the transaction is complete, the `createAuction.js` application will query "objectType": "auction", "item": "painting", "seller": "eDUwOTo6Q049c2VsbGVyLE9VPWNsaWVudCtPVT1vcmcxK09VPWRlcGFydG1lbnQxOjpDTj1jYS5vcmcxLmV4YW1wbGUuY29tLE89b3JnMS5leGFtcGxlLmNvbSxMPUR1cmhhbSxTVD1Ob3J0aCBDYXJvbGluYSxDPVVT", - "quantity": 1, "organizations": [ "Org1MSP" ], "privateBids": {}, "revealedBids": {}, - "winners": [], + "winner": "", "price": 0, "status": "open" } @@ -113,56 +112,54 @@ We can now use the bidder wallets to submit bids to the auction: Bidder1 will create a bid to purchase the painting for 800 dollars. ``` -node bid.js org1 bidder1 auction1 1 800 +node bid.js org1 bidder1 PaintingAuction 800 ``` The application will query the bid after it is created: ``` *** Result: Bid: { "objectType": "bid", - "quantity": 1, "price": 800, "org": "Org1MSP", - "buyer": "eDUwOTo6Q049YmlkZGVyMSxPVT1jbGllbnQrT1U9b3JnMStPVT1kZXBhcnRtZW50MTo6Q049Y2Eub3JnMS5leGFtcGxlLmNvbSxPPW9yZzEuZXhhbXBsZS5jb20sTD1EdXJoYW0sU1Q9Tm9ydGggQ2Fyb2xpbmEsQz1VUw==" + "bidder": "eDUwOTo6Q049YmlkZGVyMSxPVT1jbGllbnQrT1U9b3JnMStPVT1kZXBhcnRtZW50MTo6Q049Y2Eub3JnMS5leGFtcGxlLmNvbSxPPW9yZzEuZXhhbXBsZS5jb20sTD1EdXJoYW0sU1Q9Tm9ydGggQ2Fyb2xpbmEsQz1VUw==" } ``` -The bid is stored in the Org1 implicit data collection. The `"buyer"` parameter is the information from the certificate of the user that created the bid. Only this identity will be able to query the bid from private state or reveal the bid during the auction. +The bid is stored in the Org1 implicit data collection. The `"bidder"` parameter is the information from the certificate of the user that created the bid. Only this identity will be able can query the bid from private state or reveal the bid during the auction. The `bid.js` application also prints the bidID: ``` -*** Result ***SAVE THIS VALUE*** BidID: 68d3de99032700d92b2d753c79d5aff2ca79378b169efb6a70ca3e0ea43acb1b +*** Result ***SAVE THIS VALUE*** BidID: 8ef83011a5fb791f75ed008337839426f6b87981519e5d58ef5ada39c3044edd ``` The BidID acts as the unique identifier for the bid. This ID allows you to query the bid using the `queryBid.js` program and add the bid to the auction. Save the bidID returned by the application as an environment variable in your terminal: ``` -export BIDDER1_BID_ID=68d3de99032700d92b2d753c79d5aff2ca79378b169efb6a70ca3e0ea43acb1b +export BIDDER1_BID_ID=8ef83011a5fb791f75ed008337839426f6b87981519e5d58ef5ada39c3044edd ``` This value will be different for each transaction, so you will need to use the value returned in your terminal. Now that the bid has been created, you can submit the bid to the auction. Run the following command to submit the bid that was just created: ``` -node submitBid.js org1 bidder1 auction1 $BIDDER1_BID_ID +node submitBid.js org1 bidder1 PaintingAuction $BIDDER1_BID_ID ``` -The hash of bid is added to the list of private bids in that have been submitted to `auction1`. Storing the hash on the public auction ledger allows users to prove the accuracy of the bids they reveal once bidding is closed. The application queries the auction to verify that the bid was added: +The hash of bid will be added to the list private bids in that have been submitted to `PaintingAuction`. Storing the hash in the public auction allows users to accurately reveal the bid after bidding is closed. The application will query the auction to verify that the bid was added: ``` *** Result: Auction: { "objectType": "auction", "item": "painting", "seller": "eDUwOTo6Q049c2VsbGVyLE9VPWNsaWVudCtPVT1vcmcxK09VPWRlcGFydG1lbnQxOjpDTj1jYS5vcmcxLmV4YW1wbGUuY29tLE89b3JnMS5leGFtcGxlLmNvbSxMPUR1cmhhbSxTVD1Ob3J0aCBDYXJvbGluYSxDPVVT", - "quantity": 1, "organizations": [ "Org1MSP" ], "privateBids": { - "\u0000bid\u0000auction1\u000003382872f8f9dc94f211385d3d127f155e22de1bee8e112dcb90beb3e78f2722\u0000": { + "\u0000bid\u0000PaintingAuction\u00008ef83011a5fb791f75ed008337839426f6b87981519e5d58ef5ada39c3044edd\u0000": { "org": "Org1MSP", - "hash": "9345d28bb40b7026cc9e14743147356ae1cac99ced8190fe72c9be69fbfc4b71" + "hash": "5cb50a17b5a21c02fc01306e3e9b54f4db67e9a440552ce898bbd7daa62dce0f" } }, "revealedBids": {}, - "winners": [], + "winner": "", "price": 0, "status": "open" } @@ -172,34 +169,34 @@ The hash of bid is added to the list of private bids in that have been submitted Let's submit another bid. Bidder2 would like to purchase the painting for 500 dollars. ``` -node bid.js org1 bidder2 auction1 1 500 +node bid.js org1 bidder2 PaintingAuction 500 ``` Save the Bid ID returned by the application: ``` -export BIDDER2_BID_ID=77dc57876a1bc5cb798863f07eaab6f041608e55c60900b2cbcf0acc7723e8ec +export BIDDER2_BID_ID=915a908c8f2c368f4a3aedd73176656af81ddfab000b11629503403f3d97b185 ``` Submit bidder2's bid to the auction: ``` -node submitBid.js org1 bidder2 auction1 $BIDDER2_BID_ID +node submitBid.js org1 bidder2 PaintingAuction $BIDDER2_BID_ID ``` ### Bid as bidder3 from Org2 Bidder3 will bid 700 dollars for the painting: ``` -node bid.js org2 bidder3 auction1 1 700 +node bid.js org2 bidder3 PaintingAuction 700 ``` Save the Bid ID returned by the application: ``` -export BIDDER3_BID_ID=7e2d0c33d0ff1030d855e5fb76f2a4eb30589549b4cb6e581da17d3705cbf77e +export BIDDER3_BID_ID=5e4e637c68833b178739575f6fe09820b019551a8cfbb43a4d172e0aa864dfad ``` Add bidder3's bid to the auction: ``` -node submitBid.js org2 bidder3 auction1 $BIDDER3_BID_ID +node submitBid.js org2 bidder3 PaintingAuction $BIDDER3_BID_ID ``` Because bidder3 belongs to Org2, submitting the bid will add Org2 to the list of participating organizations. You can see the Org2 MSP ID has been added to the list of `"organizations"` in the updated auction returned by the application: @@ -208,56 +205,55 @@ Because bidder3 belongs to Org2, submitting the bid will add Org2 to the list of "objectType": "auction", "item": "painting", "seller": "eDUwOTo6Q049c2VsbGVyLE9VPWNsaWVudCtPVT1vcmcxK09VPWRlcGFydG1lbnQxOjpDTj1jYS5vcmcxLmV4YW1wbGUuY29tLE89b3JnMS5leGFtcGxlLmNvbSxMPUR1cmhhbSxTVD1Ob3J0aCBDYXJvbGluYSxDPVVT", - "quantity": 1, "organizations": [ "Org1MSP", "Org2MSP" ], "privateBids": { - "\u0000bid\u0000auction1\u000003382872f8f9dc94f211385d3d127f155e22de1bee8e112dcb90beb3e78f2722\u0000": { - "org": "Org1MSP", - "hash": "9345d28bb40b7026cc9e14743147356ae1cac99ced8190fe72c9be69fbfc4b71" - }, - "\u0000bid\u0000auction1\u0000347e08fbbf766a0f3678c3c6e05b0613026fa4d4619ad80869773938ed893f8d\u0000": { + "\u0000bid\u0000PaintingAuction\u00005e4e637c68833b178739575f6fe09820b019551a8cfbb43a4d172e0aa864dfad\u0000": { "org": "Org2MSP", - "hash": "ecaf72ecd6daf27889434491b0e5580c94bd682d8b0283c142b085d51fcf1f69" + "hash": "40107eab7a99dfc2f25d02b8ab840f12fd802a9f86d8d42b78d7b4409b2c15bd" }, - "\u0000bid\u0000auction1\u0000e624a5f04fb8878e0d20bf10649f06113771eba6ea4c58f46d17264d1b492a46\u0000": { + "\u0000bid\u0000PaintingAuction\u00008ef83011a5fb791f75ed008337839426f6b87981519e5d58ef5ada39c3044edd\u0000": { "org": "Org1MSP", - "hash": "8434bfe683c4436484176e6c9b21a5d684c0710fce9fd2506af4e05bb0f8a3ea" + "hash": "5cb50a17b5a21c02fc01306e3e9b54f4db67e9a440552ce898bbd7daa62dce0f" + }, + "\u0000bid\u0000PaintingAuction\u0000915a908c8f2c368f4a3aedd73176656af81ddfab000b11629503403f3d97b185\u0000": { + "org": "Org1MSP", + "hash": "a458df18b12dffe4ae6d56a270134c2d55bd53fface034bd24381d0073d46a45" } }, "revealedBids": {}, - "winners": [], + "winner": "", "price": 0, "status": "open" } ``` -Now that a bid from Org2 has been added to the auction, any updates to the auction need to be endorsed by the Org2 peer. The applications will use the `"organizations"` field to specify which organizations need to endorse submitting a new bid, revealing a bid, or updating the auction status. +Now that a bid from Org2 has been added to the auction, any updates to the auction need to be endorsed by the Org2 peer. The applications will use `"organizations"` field to specify which organizations need to endorse submitting a new bid, revealing a bid, or updating the auction status. ### Bid as bidder4 Bidder4 from Org2 would like to purchase the painting for 900 dollars: ``` -node bid.js org2 bidder4 auction1 1 900 +node bid.js org2 bidder4 PaintingAuction 900 ``` Save the Bid ID returned by the application: ``` -export BIDDER4_BID_ID=083478f1af2ba391a5b8d7c590cfb790aedf11578d64ebc4e5efe793555ff212 +export BIDDER4_BID_ID=49466271ae879bd009e75a60730a12bfa986e75f263202ab81ccd3deec544a35 ``` Add bidder4's bid to the auction: ``` -node submitBid.js org2 bidder4 auction1 $BIDDER4_BID_ID +node submitBid.js org2 bidder4 PaintingAuction $BIDDER4_BID_ID ``` ## Close the auction Now that all four bidders have joined the auction, the seller would like to close the auction and allow buyers to reveal their bids. The seller identity that created the auction needs to submit the transaction: ``` -node closeAuction.js org1 seller auction1 +node closeAuction.js org1 seller PaintingAuction ``` The application will query the auction to allow you to verify that the auction status has changed to closed. As a test, you can try to create and submit a new bid to verify that no new bids can be added to the auction. @@ -272,48 +268,46 @@ After the auction is closed, bidders can try to win the auction by revealing the Use the `revealBid.js` application to reveal the bid of Bidder1: ``` -node revealBid.js org1 bidder1 auction1 $BIDDER1_BID_ID +node revealBid.js org1 bidder1 PaintingAuction $BIDDER1_BID_ID ``` -The full bid details, including the quantity and price, are now visible: +The full bid details, including the price, are now visible: ``` *** Result: Auction: { "objectType": "auction", "item": "painting", "seller": "eDUwOTo6Q049c2VsbGVyLE9VPWNsaWVudCtPVT1vcmcxK09VPWRlcGFydG1lbnQxOjpDTj1jYS5vcmcxLmV4YW1wbGUuY29tLE89b3JnMS5leGFtcGxlLmNvbSxMPUR1cmhhbSxTVD1Ob3J0aCBDYXJvbGluYSxDPVVT", - "quantity": 100, "organizations": [ "Org1MSP", "Org2MSP" ], "privateBids": { - "\u0000bid\u0000auction1\u00002fcaee344b361a95701dc03ca4417c6c158845f9e4be3b2555b17c2b691c5ec6\u0000": { + "\u0000bid\u0000PaintingAuction\u000049466271ae879bd009e75a60730a12bfa986e75f263202ab81ccd3deec544a35\u0000": { "org": "Org2MSP", - "hash": "d6f661d8b664244ce55065edc2fd95a982221888bb19afdad931b185e187ed4f" + "hash": "b8eaeb4422b93abdfe4ccb6aa11b745b3d1cb072a99bd3eb3618f081fb1b1f89" }, - "\u0000bid\u0000auction1\u000083d529d37ea3518c67135fa8b9bf0bff33053bed0e2f38cb6a7ff75882efcf95\u0000": { + "\u0000bid\u0000PaintingAuction\u00005e4e637c68833b178739575f6fe09820b019551a8cfbb43a4d172e0aa864dfad\u0000": { "org": "Org2MSP", - "hash": "4446c7eb0e2d64165a916ee996348a18716f4c97e632d58d5a8c20eeec5a9238" + "hash": "40107eab7a99dfc2f25d02b8ab840f12fd802a9f86d8d42b78d7b4409b2c15bd" }, - "\u0000bid\u0000auction1\u00009e445520af04c8a3c05b484408f0406c9b09503903a9ed9b093f5d894f83dfea\u0000": { + "\u0000bid\u0000PaintingAuction\u00008ef83011a5fb791f75ed008337839426f6b87981519e5d58ef5ada39c3044edd\u0000": { "org": "Org1MSP", - "hash": "584dbad2269a44afb42bdbc7a7a4c08a7cd50deece1eb3fc38d4e49b9342f270" + "hash": "5cb50a17b5a21c02fc01306e3e9b54f4db67e9a440552ce898bbd7daa62dce0f" }, - "\u0000bid\u0000auction1\u0000f3304fd81664f304706ff39b5a978af94bc3e055e5bf32aed1c25b35df24c136\u0000": { + "\u0000bid\u0000PaintingAuction\u0000915a908c8f2c368f4a3aedd73176656af81ddfab000b11629503403f3d97b185\u0000": { "org": "Org1MSP", - "hash": "bbcd0c7c376e6681a76d8c5482c97f8bdcda55c90c5478100c3aef17815c4fd3" + "hash": "a458df18b12dffe4ae6d56a270134c2d55bd53fface034bd24381d0073d46a45" } }, "revealedBids": { - "\u0000bid\u0000auction1\u00009e445520af04c8a3c05b484408f0406c9b09503903a9ed9b093f5d894f83dfea\u0000": { + "\u0000bid\u0000PaintingAuction\u00008ef83011a5fb791f75ed008337839426f6b87981519e5d58ef5ada39c3044edd\u0000": { "objectType": "bid", - "quantity": 1, "price": 800, "org": "Org1MSP", - "buyer": "eDUwOTo6Q049YmlkZGVyMSxPVT1jbGllbnQrT1U9b3JnMStPVT1kZXBhcnRtZW50MTo6Q049Y2Eub3JnMS5leGFtcGxlLmNvbSxPPW9yZzEuZXhhbXBsZS5jb20sTD1EdXJoYW0sU1Q9Tm9ydGggQ2Fyb2xpbmEsQz1VUw==" + "bidder": "eDUwOTo6Q049YmlkZGVyMSxPVT1jbGllbnQrT1U9b3JnMStPVT1kZXBhcnRtZW50MTo6Q049Y2Eub3JnMS5leGFtcGxlLmNvbSxPPW9yZzEuZXhhbXBsZS5jb20sTD1EdXJoYW0sU1Q9Tm9ydGggQ2Fyb2xpbmEsQz1VUw==" } }, - "winners": [], + "winner": "", "price": 0, "status": "closed" } @@ -321,20 +315,28 @@ The full bid details, including the quantity and price, are now visible: Bidder3 from Org2 will also reveal their bid: ``` -node revealBid.js org2 bidder3 auction1 $BIDDER3_BID_ID +node revealBid.js org2 bidder3 PaintingAuction $BIDDER3_BID_ID ``` -If the action ended now, Bidder1 would win the auction. Let's try to end the auction using the seller identity and see what happens. +If the auction ended now, Bidder1 would win. Let's try to end the auction using the seller identity and see what happens. ``` -node endAuction.js org1 seller auction1 +node endAuction.js org1 seller PaintingAuction ``` -Instead of ending the auction, the transaction results in an endorsement policy failure. The end of the auction needs to be endorsed by Org2. Before endorsing the transaction, the Org2 peer queries the Org2 implicit data collection to look for a winning bid that has not yet been revealed. Because Bidder4 created a bid that is above the winning price, the Org2 peer refuses to endorse the transaction that ends the auction. +The output should look something like the following: + +``` +--> Submit the transaction to end the auction +2020-11-06T13:16:11.591Z - warn: [TransactionEventHandler]: strategyFail: commit failure for transaction "99feade5b7ec223839200867b57d18971c3e9f923efc95aaeec720727f927366": TransactionError: Commit of transaction 99feade5b7ec223839200867b57d18971c3e9f923efc95aaeec720727f927366 failed on peer peer0.org1.example.com:7051 with status ENDORSEMENT_POLICY_FAILURE +******** FAILED to submit bid: TransactionError: Commit of transaction 99feade5b7ec223839200867b57d18971c3e9f923efc95aaeec720727f927366 failed on peer peer0.org1.example.com:7051 with status ENDORSEMENT_POLICY_FAILURE +``` + +Instead of ending the auction, the transaction results in an endorsement policy failure. The end of the auction needs to be endorsed by Org2. Before endorsing the transaction, the Org2 peer queries its private data collection for any winning bids that have not yet been revealed. Because Bidder4 created a bid that is above the winning price, the Org2 peer refuses to endorse the transaction that would end the auction. Before we can end the auction, we need to reveal the bid from bidder4. ``` -node revealBid.js org2 bidder4 auction1 $BIDDER4_BID_ID +node revealBid.js org2 bidder4 PaintingAuction $BIDDER4_BID_ID ``` Bidder2 from Org1 would not win the auction in either case. As a result, Bidder2 decides not to reveal their bid. @@ -343,67 +345,58 @@ Bidder2 from Org1 would not win the auction in either case. As a result, Bidder2 Now that the winning bids have been revealed, we can end the auction: ``` -node endAuction org1 seller auction1 +node endAuction org1 seller PaintingAuction ``` -The transaction was successfully endorsed by both Org1 and Org2, who both calculated the same price and winner of the auction. +The transaction was successfully endorsed by both Org1 and Org2, who both calculated the same price and winner. The winning bidder is listed along with the price: ``` *** Result: Auction: { "objectType": "auction", "item": "painting", "seller": "eDUwOTo6Q049c2VsbGVyLE9VPWNsaWVudCtPVT1vcmcxK09VPWRlcGFydG1lbnQxOjpDTj1jYS5vcmcxLmV4YW1wbGUuY29tLE89b3JnMS5leGFtcGxlLmNvbSxMPUR1cmhhbSxTVD1Ob3J0aCBDYXJvbGluYSxDPVVT", - "quantity": 1, "organizations": [ "Org1MSP", "Org2MSP" ], "privateBids": { - "\u0000bid\u0000auction1\u000003382872f8f9dc94f211385d3d127f155e22de1bee8e112dcb90beb3e78f2722\u0000": { - "org": "Org1MSP", - "hash": "9345d28bb40b7026cc9e14743147356ae1cac99ced8190fe72c9be69fbfc4b71" - }, - "\u0000bid\u0000auction1\u0000347e08fbbf766a0f3678c3c6e05b0613026fa4d4619ad80869773938ed893f8d\u0000": { + "\u0000bid\u0000PaintingAuction\u000049466271ae879bd009e75a60730a12bfa986e75f263202ab81ccd3deec544a35\u0000": { "org": "Org2MSP", - "hash": "ecaf72ecd6daf27889434491b0e5580c94bd682d8b0283c142b085d51fcf1f69" + "hash": "b8eaeb4422b93abdfe4ccb6aa11b745b3d1cb072a99bd3eb3618f081fb1b1f89" }, - "\u0000bid\u0000auction1\u0000e624a5f04fb8878e0d20bf10649f06113771eba6ea4c58f46d17264d1b492a46\u0000": { - "org": "Org1MSP", - "hash": "8434bfe683c4436484176e6c9b21a5d684c0710fce9fd2506af4e05bb0f8a3ea" - }, - "\u0000bid\u0000auction1\u0000f67eed2c00812d02482d40a652d353c505f3161cd6837077443ee2f425413f5c\u0000": { + "\u0000bid\u0000PaintingAuction\u00005e4e637c68833b178739575f6fe09820b019551a8cfbb43a4d172e0aa864dfad\u0000": { "org": "Org2MSP", - "hash": "0c49d016c894b5623bc0be211409ebb0561c272c65f9faf2959002b0168b238c" + "hash": "40107eab7a99dfc2f25d02b8ab840f12fd802a9f86d8d42b78d7b4409b2c15bd" + }, + "\u0000bid\u0000PaintingAuction\u00008ef83011a5fb791f75ed008337839426f6b87981519e5d58ef5ada39c3044edd\u0000": { + "org": "Org1MSP", + "hash": "5cb50a17b5a21c02fc01306e3e9b54f4db67e9a440552ce898bbd7daa62dce0f" + }, + "\u0000bid\u0000PaintingAuction\u0000915a908c8f2c368f4a3aedd73176656af81ddfab000b11629503403f3d97b185\u0000": { + "org": "Org1MSP", + "hash": "a458df18b12dffe4ae6d56a270134c2d55bd53fface034bd24381d0073d46a45" } }, "revealedBids": { - "\u0000bid\u0000auction1\u000003382872f8f9dc94f211385d3d127f155e22de1bee8e112dcb90beb3e78f2722\u0000": { + "\u0000bid\u0000PaintingAuction\u000049466271ae879bd009e75a60730a12bfa986e75f263202ab81ccd3deec544a35\u0000": { "objectType": "bid", - "quantity": 1, - "price": 800, - "org": "Org1MSP", - "buyer": "eDUwOTo6Q049YmlkZGVyMSxPVT1jbGllbnQrT1U9b3JnMStPVT1kZXBhcnRtZW50MTo6Q049Y2Eub3JnMS5leGFtcGxlLmNvbSxPPW9yZzEuZXhhbXBsZS5jb20sTD1EdXJoYW0sU1Q9Tm9ydGggQ2Fyb2xpbmEsQz1VUw==" - }, - "\u0000bid\u0000auction1\u0000347e08fbbf766a0f3678c3c6e05b0613026fa4d4619ad80869773938ed893f8d\u0000": { - "objectType": "bid", - "quantity": 1, - "price": 700, - "org": "Org2MSP", - "buyer": "eDUwOTo6Q049YmlkZGVyMyxPVT1jbGllbnQrT1U9b3JnMitPVT1kZXBhcnRtZW50MTo6Q049Y2Eub3JnMi5leGFtcGxlLmNvbSxPPW9yZzIuZXhhbXBsZS5jb20sTD1IdXJzbGV5LFNUPUhhbXBzaGlyZSxDPVVL" - }, - "\u0000bid\u0000auction1\u0000f67eed2c00812d02482d40a652d353c505f3161cd6837077443ee2f425413f5c\u0000": { - "objectType": "bid", - "quantity": 1, "price": 900, "org": "Org2MSP", - "buyer": "eDUwOTo6Q049YmlkZGVyNCxPVT1jbGllbnQrT1U9b3JnMitPVT1kZXBhcnRtZW50MTo6Q049Y2Eub3JnMi5leGFtcGxlLmNvbSxPPW9yZzIuZXhhbXBsZS5jb20sTD1IdXJzbGV5LFNUPUhhbXBzaGlyZSxDPVVL" + "bidder": "eDUwOTo6Q049YmlkZGVyNCxPVT1jbGllbnQrT1U9b3JnMitPVT1kZXBhcnRtZW50MTo6Q049Y2Eub3JnMi5leGFtcGxlLmNvbSxPPW9yZzIuZXhhbXBsZS5jb20sTD1IdXJzbGV5LFNUPUhhbXBzaGlyZSxDPVVL" + }, + "\u0000bid\u0000PaintingAuction\u00005e4e637c68833b178739575f6fe09820b019551a8cfbb43a4d172e0aa864dfad\u0000": { + "objectType": "bid", + "price": 700, + "org": "Org2MSP", + "bidder": "eDUwOTo6Q049YmlkZGVyMyxPVT1jbGllbnQrT1U9b3JnMitPVT1kZXBhcnRtZW50MTo6Q049Y2Eub3JnMi5leGFtcGxlLmNvbSxPPW9yZzIuZXhhbXBsZS5jb20sTD1IdXJzbGV5LFNUPUhhbXBzaGlyZSxDPVVL" + }, + "\u0000bid\u0000PaintingAuction\u00008ef83011a5fb791f75ed008337839426f6b87981519e5d58ef5ada39c3044edd\u0000": { + "objectType": "bid", + "price": 800, + "org": "Org1MSP", + "bidder": "eDUwOTo6Q049YmlkZGVyMSxPVT1jbGllbnQrT1U9b3JnMStPVT1kZXBhcnRtZW50MTo6Q049Y2Eub3JnMS5leGFtcGxlLmNvbSxPPW9yZzEuZXhhbXBsZS5jb20sTD1EdXJoYW0sU1Q9Tm9ydGggQ2Fyb2xpbmEsQz1VUw==" } }, - "winners": [ - { - "buyer": "eDUwOTo6Q049YmlkZGVyNCxPVT1jbGllbnQrT1U9b3JnMitPVT1kZXBhcnRtZW50MTo6Q049Y2Eub3JnMi5leGFtcGxlLmNvbSxPPW9yZzIuZXhhbXBsZS5jb20sTD1IdXJzbGV5LFNUPUhhbXBzaGlyZSxDPVVL", - "quantity": 1 - } - ], + "winner": "eDUwOTo6Q049YmlkZGVyNCxPVT1jbGllbnQrT1U9b3JnMitPVT1kZXBhcnRtZW50MTo6Q049Y2Eub3JnMi5leGFtcGxlLmNvbSxPPW9yZzIuZXhhbXBsZS5jb20sTD1IdXJzbGV5LFNUPUhhbXBzaGlyZSxDPVVL", "price": 900, "status": "ended" } diff --git a/auction/application-javascript/bid.js b/auction/application-javascript/bid.js index b78ebd2c..a5540d80 100644 --- a/auction/application-javascript/bid.js +++ b/auction/application-javascript/bid.js @@ -23,7 +23,7 @@ function prettyJSONString(inputString) { } } -async function bid(ccp,wallet,user,orgMSP,auctionID,quantity,price) { +async function bid(ccp,wallet,user,orgMSP,auctionID,price) { try { const gateway = new Gateway(); @@ -36,10 +36,10 @@ async function bid(ccp,wallet,user,orgMSP,auctionID,quantity,price) { const contract = network.getContract(myChaincodeName); console.log('\n--> Evaluate Transaction: get your client ID'); - let buyer = await contract.evaluateTransaction('GetID'); - console.log('*** Result: Buyer ID is ' + buyer.toString()); + let bidder = await contract.evaluateTransaction('GetID'); + console.log('*** Result: Bidder ID is ' + bidder.toString()); - let bidData = { objectType: 'bid', quantity: parseInt(quantity) , price: parseInt(price), org: orgMSP, buyer: buyer.toString()}; + let bidData = { objectType: 'bid', price: parseInt(price), org: orgMSP, bidder: bidder.toString()}; let statefulTxn = contract.createTransaction('Bid'); statefulTxn.setEndorsingOrganizations(orgMSP); @@ -50,7 +50,7 @@ async function bid(ccp,wallet,user,orgMSP,auctionID,quantity,price) { let bidID = statefulTxn.getTransactionId(); - console.log('\n--> Submit Transaction: Create the bid that is stored in your private data collection of your organization'); + console.log('\n--> Submit Transaction: Create the bid that is stored in your organization\'s private data collection'); await statefulTxn.submit(auctionID); console.log('*** Result: committed'); console.log('*** Result ***SAVE THIS VALUE*** BidID: ' + bidID.toString()); @@ -73,17 +73,15 @@ async function main() { try { if (process.argv[2] == undefined || process.argv[3] == undefined - || process.argv[4] == undefined || process.argv[5] == undefined - || process.argv[6] == undefined) { - console.log("Usage: node bid.js org userID auctionID quantity price"); + || process.argv[4] == undefined || process.argv[5] == undefined) { + console.log("Usage: node bid.js org userID auctionID price"); process.exit(1); } const org = process.argv[2] const user = process.argv[3]; const auctionID = process.argv[4]; - const quantity = process.argv[5]; - const price = process.argv[6]; + const price = process.argv[5]; if (org == 'Org1' || org == 'org1') { @@ -91,7 +89,7 @@ async function main() { const ccp = buildCCPOrg1(); const walletPath = path.join(__dirname, 'wallet/org1'); const wallet = await buildWallet(Wallets, walletPath); - await bid(ccp,wallet,user,orgMSP,auctionID,quantity,price); + await bid(ccp,wallet,user,orgMSP,auctionID,price); } else if (org == 'Org2' || org == 'org2') { @@ -99,9 +97,9 @@ async function main() { const ccp = buildCCPOrg2(); const walletPath = path.join(__dirname, 'wallet/org2'); const wallet = await buildWallet(Wallets, walletPath); - await bid(ccp,wallet,user,orgMSP,auctionID,quantity,price); + await bid(ccp,wallet,user,orgMSP,auctionID,price); } else { - console.log("Usage: node bid.js org userID auctionID quantity price"); + console.log("Usage: node bid.js org userID auctionID price"); console.log("Org must be Org1 or Org2"); } } catch (error) { diff --git a/auction/application-javascript/createAuction.js b/auction/application-javascript/createAuction.js index ca47e9d5..8d083d70 100644 --- a/auction/application-javascript/createAuction.js +++ b/auction/application-javascript/createAuction.js @@ -23,7 +23,7 @@ function prettyJSONString(inputString) { } } -async function createAuction(ccp,wallet,user,auctionID,item,quantity) { +async function createAuction(ccp,wallet,user,auctionID,item) { try { const gateway = new Gateway(); @@ -38,7 +38,7 @@ async function createAuction(ccp,wallet,user,auctionID,item,quantity) { let statefulTxn = contract.createTransaction('CreateAuction'); console.log('\n--> Submit Transaction: Propose a new auction'); - await statefulTxn.submit(auctionID,item,parseInt(quantity)); + await statefulTxn.submit(auctionID,item); console.log('*** Result: committed'); console.log('\n--> Evaluate Transaction: query the auction that was just created'); @@ -55,9 +55,8 @@ async function main() { try { if (process.argv[2] == undefined || process.argv[3] == undefined - || process.argv[4] == undefined || process.argv[5] == undefined - || process.argv[6] == undefined) { - console.log("Usage: node createAuction.js org userID auctionID item quantity"); + || process.argv[4] == undefined || process.argv[5] == undefined) { + console.log("Usage: node createAuction.js org userID auctionID item"); process.exit(1); } @@ -65,7 +64,6 @@ async function main() { const user = process.argv[3]; const auctionID = process.argv[4]; const item = process.argv[5]; - const quantity = process.argv[6]; if (org == 'Org1' || org == 'org1') { @@ -73,7 +71,7 @@ async function main() { const ccp = buildCCPOrg1(); const walletPath = path.join(__dirname, 'wallet/org1'); const wallet = await buildWallet(Wallets, walletPath); - await createAuction(ccp,wallet,user,auctionID,item,quantity); + await createAuction(ccp,wallet,user,auctionID,item); } else if (org == 'Org2' || org == 'org2') { @@ -81,9 +79,9 @@ async function main() { const ccp = buildCCPOrg2(); const walletPath = path.join(__dirname, 'wallet/org2'); const wallet = await buildWallet(Wallets, walletPath); - await createAuction(ccp,wallet,user,auctionID,item,quantity); + await createAuction(ccp,wallet,user,auctionID,item); } else { - console.log("Usage: node createAuction.js org userID auctionID item quantity"); + console.log("Usage: node createAuction.js org userID auctionID item"); console.log("Org must be Org1 or Org2"); } } catch (error) { diff --git a/auction/application-javascript/revealBid.js b/auction/application-javascript/revealBid.js index ce761748..8b397439 100644 --- a/auction/application-javascript/revealBid.js +++ b/auction/application-javascript/revealBid.js @@ -44,7 +44,7 @@ async function addBid(ccp,wallet,user,auctionID,bidID) { // console.log('*** Result: Bid: ' + prettyJSONString(auctionString.toString())); var auctionJSON = JSON.parse(auctionString); - let bidData = { objectType: 'bid', quantity: parseInt(bidJSON.quantity) , price: parseInt(bidJSON.price), org: bidJSON.org, buyer: bidJSON.buyer}; + let bidData = { objectType: 'bid', price: parseInt(bidJSON.price), org: bidJSON.org, bidder: bidJSON.bidder}; console.log('*** Result: Bid: ' + JSON.stringify(bidData,null,2)); let statefulTxn = contract.createTransaction('RevealBid'); diff --git a/auction/chaincode-go/smart-contract/auction.go b/auction/chaincode-go/smart-contract/auction.go index 1f131a05..c197e7e5 100644 --- a/auction/chaincode-go/smart-contract/auction.go +++ b/auction/chaincode-go/smart-contract/auction.go @@ -9,7 +9,6 @@ import ( "crypto/sha256" "encoding/json" "fmt" - "sort" "github.com/hyperledger/fabric-contract-api-go/contractapi" ) @@ -23,11 +22,10 @@ type Auction struct { Type string `json:"objectType"` ItemSold string `json:"item"` Seller string `json:"seller"` - Quantity int `json:"quantity"` Orgs []string `json:"organizations"` PrivateBids map[string]BidHash `json:"privateBids"` RevealedBids map[string]FullBid `json:"revealedBids"` - Winners []Winners `json:"winners"` + Winner string `json:"winner"` Price int `json:"price"` Status string `json:"status"` } @@ -35,10 +33,9 @@ type Auction struct { // FullBid is the structure of a revealed bid type FullBid struct { Type string `json:"objectType"` - Quantity int `json:"quantity"` Price int `json:"price"` Org string `json:"org"` - Buyer string `json:"buyer"` + Bidder string `json:"bidder"` } // BidHash is the structure of a private bid @@ -47,17 +44,11 @@ type BidHash struct { Hash string `json:"hash"` } -// Winners stores the winners of the auction -type Winners struct { - Buyer string `json:"buyer"` - Quantity int `json:"quantity"` -} - const bidKeyType = "bid" // CreateAuction creates on auction on the public channel. The identity that // submits the transacion becomes the seller of the auction -func (s *SmartContract) CreateAuction(ctx contractapi.TransactionContextInterface, auctionID string, itemsold string, quantity int) error { +func (s *SmartContract) CreateAuction(ctx contractapi.TransactionContextInterface, auctionID string, itemsold string) error { // get ID of submitting client clientID, err := ctx.GetClientIdentity().GetID() @@ -78,13 +69,12 @@ func (s *SmartContract) CreateAuction(ctx contractapi.TransactionContextInterfac auction := Auction{ Type: "auction", ItemSold: itemsold, - Quantity: quantity, Price: 0, Seller: clientID, Orgs: []string{clientOrgID}, PrivateBids: bidders, RevealedBids: revealedBids, - Winners: []Winners{}, + Winner: "", Status: "open", } @@ -108,7 +98,7 @@ func (s *SmartContract) CreateAuction(ctx contractapi.TransactionContextInterfac return nil } -// Bid is used to add a users bid to the auction. The bid is stored in the private +// Bid is used to add a user's bid to the auction. The bid is stored in the private // data collection on the peer of the bidder's organization. The function returns // the transaction ID so that users can identify and query their bid func (s *SmartContract) Bid(ctx contractapi.TransactionContextInterface, auctionID string) (string, error) { @@ -196,7 +186,7 @@ func (s *SmartContract) SubmitBid(ctx contractapi.TransactionContextInterface, a return fmt.Errorf("failed to create composite key: %v", err) } - // get the hash of the bid if found in private collection + // get the hash of the bid stored in private data collection bidHash, err := ctx.GetStub().GetPrivateDataHash(collection, bidKey) if err != nil { return fmt.Errorf("failed to read bid bash from collection: %v", err) @@ -216,7 +206,7 @@ func (s *SmartContract) SubmitBid(ctx contractapi.TransactionContextInterface, a bidders[bidKey] = NewHash auctionJSON.PrivateBids = bidders - // Add the bidding organization to the list of participating organization's if it is not already + // Add the bidding organization to the list of participating organizations if it is not already Orgs := auctionJSON.Orgs if !(contains(Orgs, clientOrgID)) { newOrgs := append(Orgs, clientOrgID) @@ -290,7 +280,7 @@ func (s *SmartContract) RevealBid(ctx contractapi.TransactionContextInterface, a // Complete a series of three checks before we add the bid to the auction - // check 1: check that the auction is closed. We cannot reveal an + // check 1: check that the auction is closed. We cannot reveal a // bid to an open auction Status := auctionJSON.Status if Status != "closed" { @@ -332,10 +322,9 @@ func (s *SmartContract) RevealBid(ctx contractapi.TransactionContextInterface, a // we can add the bid to the auction if all checks have passed type transientBidInput struct { - Quantity int `json:"quantity"` Price int `json:"price"` Org string `json:"org"` - Buyer string `json:"buyer"` + Bidder string `json:"bidder"` } // unmarshal bid imput @@ -354,14 +343,13 @@ func (s *SmartContract) RevealBid(ctx contractapi.TransactionContextInterface, a // marshal transient parameters and ID and MSPID into bid object NewBid := FullBid{ Type: bidKeyType, - Quantity: bidInput.Quantity, Price: bidInput.Price, Org: bidInput.Org, - Buyer: bidInput.Buyer, + Bidder: bidInput.Bidder, } // check 4: make sure that the transaction is being submitted is the bidder - if bidInput.Buyer != clientID { + if bidInput.Bidder != clientID { return fmt.Errorf("Permission denied, client id %v is not the owner of the bid", clientID) } @@ -468,57 +456,17 @@ func (s *SmartContract) EndAuction(ctx contractapi.TransactionContextInterface, } // get the list of revealed bids - revealedBidMap := auctionJSON.RevealedBids if len(auctionJSON.RevealedBids) == 0 { return fmt.Errorf("No bids have been revealed, cannot end auction: %v", err) } - // sort the map of revealed bids to make it easier to calculate winners - // if bids are tied, fill smaller bids first - var Bidders []FullBid - + // determine the highest bid for _, bid := range revealedBidMap { - Bidders = append(Bidders, bid) - } - - sort.Slice(Bidders, func(p, q int) bool { - if Bidders[p].Price > Bidders[q].Price { - return true + if bid.Price > auctionJSON.Price { + auctionJSON.Winner = bid.Bidder + auctionJSON.Price = bid.Price } - if Bidders[p].Price < Bidders[q].Price { - return false - } - return Bidders[p].Quantity < Bidders[q].Quantity - }) - - i := 0 - remainingQuantity := auctionJSON.Quantity - - // calculate the winners - for remainingQuantity > 0 { - - // create the next winning bid - winner := Winners{ - Buyer: Bidders[i].Buyer, - Quantity: Bidders[i].Quantity, - } - - // add them to the list of winners and change the winning price - auctionJSON.Winners = append(auctionJSON.Winners, winner) - auctionJSON.Price = Bidders[i].Price - - // Calculate the quantity that goes to the winner - // if there is sufficient quantity to give them the full bid - if remainingQuantity > Bidders[i].Quantity { - remainingQuantity = remainingQuantity - Bidders[i].Quantity - - // if there is not, give the remainder - } else { - auctionJSON.Winners[i].Quantity = remainingQuantity - remainingQuantity = 0 - } - i++ } // check if there is a winning bid that has yet to be revealed diff --git a/auction/chaincode-go/smart-contract/auctionQueries.go b/auction/chaincode-go/smart-contract/auctionQueries.go index 6ba09f28..64eb45bf 100644 --- a/auction/chaincode-go/smart-contract/auctionQueries.go +++ b/auction/chaincode-go/smart-contract/auctionQueries.go @@ -70,7 +70,7 @@ func (s *SmartContract) QueryBid(ctx contractapi.TransactionContextInterface, au } // check that the client querying the bid is the bid owner - if bid.Buyer != clientID { + if bid.Bidder != clientID { return nil, fmt.Errorf("Permission denied, client id %v is not the owner of the bid", clientID) } diff --git a/auction/dutch-auction/README.md b/auction/dutch-auction/README.md deleted file mode 100644 index cfe45054..00000000 --- a/auction/dutch-auction/README.md +++ /dev/null @@ -1,431 +0,0 @@ -## Running a Dutch auction - -The auction sample smart contract can be used to implement a [Dutch auction](https://en.wikipedia.org/wiki/Dutch_auction) to sell multiple items of the same good. All items are sold at the price that clears the auction. This tutorial creates an auction to sell 100 tickets to multiple bidders. - -## Deploy the chaincode - -Change into the test network directory. -``` -cd fabric-samples/test-network -``` - -If the test network is already running, run the following command to bring the network down and start from a clean initial state. -``` -./network.sh down -``` - -You can then run the following command to deploy a new network. -``` -./network.sh up createChannel -ca -``` - -Run the following command to deploy the auction smart contract. -``` -./network.sh deployCC -ccn auction -ccp ../auction/chaincode-go/ -ccep "OR('Org1MSP.peer','Org2MSP.peer')" -``` - -## Install the application dependencies - -Change into the `application-javascript` directory: -``` -cd fabric-samples/auction/application-javascript -``` - -From this directory, run the following command to download the application dependencies if you have not done so already: -``` -npm install -``` - -## Register and enroll the application identities - -To interact with the network, you will need to enroll the Certificate Authority administrators of Org1 and Org2. You can use the `enrollAdmin.js` program for this task. Run the following command to enroll the Org1 admin: -``` -node enrollAdmin.js org1 -``` -You should see the logs of the admin wallet being created on your local file system. Now run the command to enroll the CA admin of Org2: -``` -node enrollAdmin.js org2 -``` - -We can use the CA admins of both organizations to register and enroll the identities of the seller that will create the auction and the bidders who will try to purchase the tickets. - -Run the following command to register and enroll the seller identity that will create the auction. The seller will belong to Org1. -``` -node registerEnrollUser.js org1 seller -``` - -You should see the logs of the seller wallet being created as well. Run the following commands to register and enroll 2 bidders from Org1 and another 3 bidders from Org2: -``` -node registerEnrollUser.js org1 bidder1 -node registerEnrollUser.js org1 bidder2 -node registerEnrollUser.js org2 bidder3 -node registerEnrollUser.js org2 bidder4 -node registerEnrollUser.js org2 bidder5 -``` - -## Create the auction - -The seller from Org1 would like to create an auction to sell 100 tickets. Run the following command to use the seller wallet to run the `createAuction.js` application. The seller needs to provide an ID for the auction, the item to be sold, and the quantity to be sold to create the auction: -``` -node createAuction.js org1 seller auction1 tickets 100 -``` - -You will see the application query the auction after it is created. - -## Bid on the auction - -We can now use the bidder wallets to submit bids to the auction: - -### Bid as bidder1 - -Bidder1 will create a bid to purchase 50 tickets for 80 dollars. -``` -node bid.js org1 bidder1 auction1 50 80 -``` - -The application will query the bid after it is created: -``` -*** Result: Bid: { - "objectType": "bid", - "quantity": 50, - "price": 80, - "org": "Org1MSP", - "buyer": "eDUwOTo6Q049YmlkZGVyMSxPVT1jbGllbnQrT1U9b3JnMStPVT1kZXBhcnRtZW50MTo6Q049Y2Eub3JnMS5leGFtcGxlLmNvbSxPPW9yZzEuZXhhbXBsZS5jb20sTD1EdXJoYW0sU1Q9Tm9ydGggQ2Fyb2xpbmEsQz1VUw==" -} -``` - -The `bid.js` application also prints the bidID: -``` -*** Result ***SAVE THIS VALUE*** BidID: 61e9b0fc1913f10872625bea4a6555522c70070416209848cc1d8fb6101133ad -``` - -The BidID acts as the unique identifier for the bid. This ID allows you to query the bid using the `queryBid.js` program and add the bid to the auction. Save the bidID returned by the application as an environment variable in your terminal: -``` -export BIDDER1_BID_ID=61e9b0fc1913f10872625bea4a6555522c70070416209848cc1d8fb6101133ad -``` -This value will be different for each transaction, so you will need to use the value returned in your terminal. - -Now that the bid has been created, you can submit the bid to the auction. Run the following command to submit the bid that was just created: -``` -node submitBid.js org1 bidder1 auction1 $BIDDER1_BID_ID -``` - -The hash of bid is added to the list of private bids in that have been submitted to `auction1`. Storing the hash on the public auction ledger allows users to prove the accuracy of the bids they reveal once bidding is closed. The application queries the auction to verify that the bid was added: -``` -*** Result: Auction: { - "objectType": "auction", - "item": "tickets", - "seller": "eDUwOTo6Q049c2VsbGVyLE9VPWNsaWVudCtPVT1vcmcxK09VPWRlcGFydG1lbnQxOjpDTj1jYS5vcmcxLmV4YW1wbGUuY29tLE89b3JnMS5leGFtcGxlLmNvbSxMPUR1cmhhbSxTVD1Ob3J0aCBDYXJvbGluYSxDPVVT", - "quantity": 100, - "organizations": [ - "Org1MSP" - ], - "privateBids": { - "\u0000bid\u0000auction1\u000061e9b0fc1913f10872625bea4a6555522c70070416209848cc1d8fb6101133ad\u0000": { - "org": "Org1MSP", - "hash": "584dbad2269a44afb42bdbc7a7a4c08a7cd50deece1eb3fc38d4e49b9342f270" - } - }, - "revealedBids": {}, - "winners": [], - "price": 0, - "status": "open" -} -``` - -### Bid as bidder2 - -Let's submit another bid. Bidder2 would like to purchase 40 tickets for 50 dollars. -``` -node bid.js org1 bidder2 auction1 40 50 -``` - -Save the Bid ID returned by the application: -``` -export BIDDER2_BID_ID=911c7920a7ba4643a531cb2d5d274d303fdb2e6800f50aeb6b725af0b7162ea2 -``` - -Submit bidder2's bid to the auction: -``` -node submitBid.js org1 bidder2 auction1 $BIDDER2_BID_ID -``` - -### Bid as bidder3 from Org2 - -Bidder3 will bid for 30 tickets at 70 dollars: -``` -node bid.js org2 bidder3 auction1 30 70 -``` - -Save the Bid ID returned by the application: -``` -export BIDDER3_BID_ID=93a8164628fa28290554b5dc6f505cbb8c7498d8f7c60f7df33d4a1cffb8fa47 -``` - -Add bidder3's bid to the auction: -``` -node submitBid.js org2 bidder3 auction1 $BIDDER3_BID_ID -``` - -Because bidder3 belongs to Org2, submitting the bid will add Org2 to the list of participating organizations. You can see the Org2 MSP ID has been added to the list of `"organizations"` in the updated auction returned by the application: -``` -*** Result: Auction: { - "objectType": "auction", - "item": "tickets", - "seller": "eDUwOTo6Q049c2VsbGVyLE9VPWNsaWVudCtPVT1vcmcxK09VPWRlcGFydG1lbnQxOjpDTj1jYS5vcmcxLmV4YW1wbGUuY29tLE89b3JnMS5leGFtcGxlLmNvbSxMPUR1cmhhbSxTVD1Ob3J0aCBDYXJvbGluYSxDPVVT", - "quantity": 100, - "organizations": [ - "Org1MSP", - "Org2MSP" - ], - "privateBids": { - "\u0000bid\u0000auction1\u000061e9b0fc1913f10872625bea4a6555522c70070416209848cc1d8fb6101133ad\u0000": { - "org": "Org1MSP", - "hash": "584dbad2269a44afb42bdbc7a7a4c08a7cd50deece1eb3fc38d4e49b9342f270" - }, - "\u0000bid\u0000auction1\u0000911c7920a7ba4643a531cb2d5d274d303fdb2e6800f50aeb6b725af0b7162ea2\u0000": { - "org": "Org1MSP", - "hash": "bbcd0c7c376e6681a76d8c5482c97f8bdcda55c90c5478100c3aef17815c4fd3" - }, - "\u0000bid\u0000auction1\u000093a8164628fa28290554b5dc6f505cbb8c7498d8f7c60f7df33d4a1cffb8fa47\u0000": { - "org": "Org2MSP", - "hash": "4446c7eb0e2d64165a916ee996348a18716f4c97e632d58d5a8c20eeec5a9238" - } - }, - "revealedBids": {}, - "winners": [], - "price": 0, - "status": "open" -} -``` - -Now that a bid from Org2 has been added to the auction, any updates to the auction need to be endorsed by the Org2 peer. The applications will use the `"organizations"` field to specify which organizations need to endorse submitting a new bid, revealing a bid, or updating the auction status. - -### Bid as bidder4 - -Bidder4 from Org2 would like to purchase 15 tickets for 60 dollars: -``` -node bid.js org2 bidder4 auction1 15 60 -``` - -Save the Bid ID returned by the application: -``` -export BIDDER4_BID_ID=324de04c459c5a38f103e9096dea06e19faca88f84dd5175ba0e6fd6a9d7d140 -``` - -Add bidder4's bid to the auction: -``` -node submitBid.js org2 bidder4 auction1 $BIDDER4_BID_ID -``` - -### Bid as bidder5 - -Bidder5 from Org2 will bid for 20 tickets at 60 dollars: -``` -node bid.js org2 bidder4 auction1 20 60 -``` - -Save the Bid ID returned by the application: -``` -export BIDDER5_BID_ID=3245049bd81a8cecfbc006f29eed1df0570e385e63daedd19c06b2c92d2067ae -``` - -Add bidder4's bid to the auction: -``` -node submitBid.js org2 bidder5 auction1 $BIDDER5_BID_ID -``` - - -## Close the auction - -Now that all five bidders have joined the auction, the seller would like to close the auction and allow buyers to reveal their bids. The seller identity that created the auction needs to submit the transaction: -``` -node closeAuction.js org1 seller auction1 -``` - -The application will query the auction to allow you to verify that the auction status has changed to closed. - -## Reveal bids - -After the auction is closed, bidders can try to win the auction by revealing their bids. The transaction to reveal a bid needs to pass four checks: -1. The auction is closed. -2. The transaction was submitted by the identity that created the bid. -3. The hash of the revealed bid matches the hash of the bid on the channel ledger. This confirms that the bid is the same as the bid that is stored in the private data collection. -4. The hash of the revealed bid matches the hash that was submitted to the auction. This confirms that the bid was not altered after the auction was closed. - -Use the `revealBid.js` application to reveal the bid of Bidder1: -``` -node revealBid.js org1 bidder1 auction1 $BIDDER1_BID_ID -``` - -The full bid details, including the quantity and price, are now visible: -``` -*** Result: Auction: { - "objectType": "auction", - "item": "tickets", - "seller": "eDUwOTo6Q049c2VsbGVyLE9VPWNsaWVudCtPVT1vcmcxK09VPWRlcGFydG1lbnQxOjpDTj1jYS5vcmcxLmV4YW1wbGUuY29tLE89b3JnMS5leGFtcGxlLmNvbSxMPUR1cmhhbSxTVD1Ob3J0aCBDYXJvbGluYSxDPVVT", - "quantity": 100, - "organizations": [ - "Org1MSP", - "Org2MSP" - ], - "privateBids": { - "\u0000bid\u0000auction1\u00003245049bd81a8cecfbc006f29eed1df0570e385e63daedd19c06b2c92d2067ae\u0000": { - "org": "Org2MSP", - "hash": "5a51070f188e6480fe606acdc5ad1a1c36330adc3547eb021aaf87aa00ec79f7" - }, - "\u0000bid\u0000auction1\u0000324de04c459c5a38f103e9096dea06e19faca88f84dd5175ba0e6fd6a9d7d140\u0000": { - "org": "Org2MSP", - "hash": "516c775f7da2fd653dab71d20d60a6e8bea8ff856803af4b59f943ca2ba40699" - }, - "\u0000bid\u0000auction1\u000061e9b0fc1913f10872625bea4a6555522c70070416209848cc1d8fb6101133ad\u0000": { - "org": "Org1MSP", - "hash": "584dbad2269a44afb42bdbc7a7a4c08a7cd50deece1eb3fc38d4e49b9342f270" - }, - "\u0000bid\u0000auction1\u0000911c7920a7ba4643a531cb2d5d274d303fdb2e6800f50aeb6b725af0b7162ea2\u0000": { - "org": "Org1MSP", - "hash": "bbcd0c7c376e6681a76d8c5482c97f8bdcda55c90c5478100c3aef17815c4fd3" - }, - "\u0000bid\u0000auction1\u000093a8164628fa28290554b5dc6f505cbb8c7498d8f7c60f7df33d4a1cffb8fa47\u0000": { - "org": "Org2MSP", - "hash": "4446c7eb0e2d64165a916ee996348a18716f4c97e632d58d5a8c20eeec5a9238" - } - }, - "revealedBids": { - "\u0000bid\u0000auction1\u000061e9b0fc1913f10872625bea4a6555522c70070416209848cc1d8fb6101133ad\u0000": { - "objectType": "bid", - "quantity": 50, - "price": 80, - "org": "Org1MSP", - "buyer": "eDUwOTo6Q049YmlkZGVyMSxPVT1jbGllbnQrT1U9b3JnMStPVT1kZXBhcnRtZW50MTo6Q049Y2Eub3JnMS5leGFtcGxlLmNvbSxPPW9yZzEuZXhhbXBsZS5jb20sTD1EdXJoYW0sU1Q9Tm9ydGggQ2Fyb2xpbmEsQz1VUw==" - } - }, - "winners": [], - "price": 0, - "status": "closed" -} -``` - -All bidders will reveal their bid to participate in the auction. Run the following commands to reveal the bids of the remaining four bidders: -``` -node revealBid.js org1 bidder2 auction1 $BIDDER2_BID_ID -node revealBid.js org2 bidder3 auction1 $BIDDER3_BID_ID -node revealBid.js org2 bidder4 auction1 $BIDDER4_BID_ID -node revealBid.js org2 bidder5 auction1 $BIDDER5_BID_ID -``` - -## End the auction - -Now that the winning bids have been revealed, we can end the auction: -``` -node endAuction org1 seller auction1 -``` - -The transaction was successfully endorsed by both Org1 and Org2, who both calculated the same price and winners of the auction. Each winning bidder is listed next to the quantity that was allocated to them. - -``` -*** Result: Auction: { - "objectType": "auction", - "item": "tickets", - "seller": "eDUwOTo6Q049c2VsbGVyLE9VPWNsaWVudCtPVT1vcmcxK09VPWRlcGFydG1lbnQxOjpDTj1jYS5vcmcxLmV4YW1wbGUuY29tLE89b3JnMS5leGFtcGxlLmNvbSxMPUR1cmhhbSxTVD1Ob3J0aCBDYXJvbGluYSxDPVVT", - "quantity": 100, - "organizations": [ - "Org1MSP", - "Org2MSP" - ], - "privateBids": { - "\u0000bid\u0000auction1\u00003245049bd81a8cecfbc006f29eed1df0570e385e63daedd19c06b2c92d2067ae\u0000": { - "org": "Org2MSP", - "hash": "5a51070f188e6480fe606acdc5ad1a1c36330adc3547eb021aaf87aa00ec79f7" - }, - "\u0000bid\u0000auction1\u0000324de04c459c5a38f103e9096dea06e19faca88f84dd5175ba0e6fd6a9d7d140\u0000": { - "org": "Org2MSP", - "hash": "516c775f7da2fd653dab71d20d60a6e8bea8ff856803af4b59f943ca2ba40699" - }, - "\u0000bid\u0000auction1\u000061e9b0fc1913f10872625bea4a6555522c70070416209848cc1d8fb6101133ad\u0000": { - "org": "Org1MSP", - "hash": "584dbad2269a44afb42bdbc7a7a4c08a7cd50deece1eb3fc38d4e49b9342f270" - }, - "\u0000bid\u0000auction1\u0000911c7920a7ba4643a531cb2d5d274d303fdb2e6800f50aeb6b725af0b7162ea2\u0000": { - "org": "Org1MSP", - "hash": "bbcd0c7c376e6681a76d8c5482c97f8bdcda55c90c5478100c3aef17815c4fd3" - }, - "\u0000bid\u0000auction1\u000093a8164628fa28290554b5dc6f505cbb8c7498d8f7c60f7df33d4a1cffb8fa47\u0000": { - "org": "Org2MSP", - "hash": "4446c7eb0e2d64165a916ee996348a18716f4c97e632d58d5a8c20eeec5a9238" - } - }, - "revealedBids": { - "\u0000bid\u0000auction1\u00003245049bd81a8cecfbc006f29eed1df0570e385e63daedd19c06b2c92d2067ae\u0000": { - "objectType": "bid", - "quantity": 20, - "price": 60, - "org": "Org2MSP", - "buyer": "eDUwOTo6Q049YmlkZGVyNCxPVT1jbGllbnQrT1U9b3JnMitPVT1kZXBhcnRtZW50MTo6Q049Y2Eub3JnMi5leGFtcGxlLmNvbSxPPW9yZzIuZXhhbXBsZS5jb20sTD1IdXJzbGV5LFNUPUhhbXBzaGlyZSxDPVVL" - }, - "\u0000bid\u0000auction1\u0000324de04c459c5a38f103e9096dea06e19faca88f84dd5175ba0e6fd6a9d7d140\u0000": { - "objectType": "bid", - "quantity": 15, - "price": 60, - "org": "Org2MSP", - "buyer": "eDUwOTo6Q049YmlkZGVyNCxPVT1jbGllbnQrT1U9b3JnMitPVT1kZXBhcnRtZW50MTo6Q049Y2Eub3JnMi5leGFtcGxlLmNvbSxPPW9yZzIuZXhhbXBsZS5jb20sTD1IdXJzbGV5LFNUPUhhbXBzaGlyZSxDPVVL" - }, - "\u0000bid\u0000auction1\u000061e9b0fc1913f10872625bea4a6555522c70070416209848cc1d8fb6101133ad\u0000": { - "objectType": "bid", - "quantity": 50, - "price": 80, - "org": "Org1MSP", - "buyer": "eDUwOTo6Q049YmlkZGVyMSxPVT1jbGllbnQrT1U9b3JnMStPVT1kZXBhcnRtZW50MTo6Q049Y2Eub3JnMS5leGFtcGxlLmNvbSxPPW9yZzEuZXhhbXBsZS5jb20sTD1EdXJoYW0sU1Q9Tm9ydGggQ2Fyb2xpbmEsQz1VUw==" - }, - "\u0000bid\u0000auction1\u0000911c7920a7ba4643a531cb2d5d274d303fdb2e6800f50aeb6b725af0b7162ea2\u0000": { - "objectType": "bid", - "quantity": 40, - "price": 50, - "org": "Org1MSP", - "buyer": "eDUwOTo6Q049YmlkZGVyMixPVT1jbGllbnQrT1U9b3JnMStPVT1kZXBhcnRtZW50MTo6Q049Y2Eub3JnMS5leGFtcGxlLmNvbSxPPW9yZzEuZXhhbXBsZS5jb20sTD1EdXJoYW0sU1Q9Tm9ydGggQ2Fyb2xpbmEsQz1VUw==" - }, - "\u0000bid\u0000auction1\u000093a8164628fa28290554b5dc6f505cbb8c7498d8f7c60f7df33d4a1cffb8fa47\u0000": { - "objectType": "bid", - "quantity": 30, - "price": 70, - "org": "Org2MSP", - "buyer": "eDUwOTo6Q049YmlkZGVyMyxPVT1jbGllbnQrT1U9b3JnMitPVT1kZXBhcnRtZW50MTo6Q049Y2Eub3JnMi5leGFtcGxlLmNvbSxPPW9yZzIuZXhhbXBsZS5jb20sTD1IdXJzbGV5LFNUPUhhbXBzaGlyZSxDPVVL" - } - }, - "winners": [ - { - "buyer": "eDUwOTo6Q049YmlkZGVyMSxPVT1jbGllbnQrT1U9b3JnMStPVT1kZXBhcnRtZW50MTo6Q049Y2Eub3JnMS5leGFtcGxlLmNvbSxPPW9yZzEuZXhhbXBsZS5jb20sTD1EdXJoYW0sU1Q9Tm9ydGggQ2Fyb2xpbmEsQz1VUw==", - "quantity": 50 - }, - { - "buyer": "eDUwOTo6Q049YmlkZGVyMyxPVT1jbGllbnQrT1U9b3JnMitPVT1kZXBhcnRtZW50MTo6Q049Y2Eub3JnMi5leGFtcGxlLmNvbSxPPW9yZzIuZXhhbXBsZS5jb20sTD1IdXJzbGV5LFNUPUhhbXBzaGlyZSxDPVVL", - "quantity": 30 - }, - { - "buyer": "eDUwOTo6Q049YmlkZGVyNCxPVT1jbGllbnQrT1U9b3JnMitPVT1kZXBhcnRtZW50MTo6Q049Y2Eub3JnMi5leGFtcGxlLmNvbSxPPW9yZzIuZXhhbXBsZS5jb20sTD1IdXJzbGV5LFNUPUhhbXBzaGlyZSxDPVVL", - "quantity": 15 - }, - { - "buyer": "eDUwOTo6Q049YmlkZGVyNCxPVT1jbGllbnQrT1U9b3JnMitPVT1kZXBhcnRtZW50MTo6Q049Y2Eub3JnMi5leGFtcGxlLmNvbSxPPW9yZzIuZXhhbXBsZS5jb20sTD1IdXJzbGV5LFNUPUhhbXBzaGlyZSxDPVVL", - "quantity": 5 - } - ], - "price": 60, - "status": "ended" -} -``` - -The auction allocates tickets to the highest bids first. Because all 100 tickets are sold after allocating tickets to the bids that were submitted at 60, 60 is the `"price"` that clears the auction. The first 80 tickets are allocated to Bidder1 and Bidder3. The remaining 20 tickers are allocated to Bidder4 and Bidder5. When bids are tied, the auction smart contract fills the smaller bids first. As a result, Bidder4 is awarded their full bid of 15 tickets, while Bidder5 is allocated the remaining 5 tickets. - -## Clean up - -When your are done using the auction smart contract, you can bring down the network and clean up the environment. In the `auction/application-javascript` directory, run the following command to remove the wallets used to run the applications: -``` -rm -rf wallet -``` - -You can then navigate to the test network directory and bring down the network: -```` -cd ../../test-network/ -./network.sh down -```` From c6956e50570527e6b83ae362e4f44e1fffc2bd95 Mon Sep 17 00:00:00 2001 From: JGONGSQ Date: Wed, 11 Nov 2020 01:22:05 +1100 Subject: [PATCH 34/38] Update asset_transfer_ledger_chaincode.js (#368) the `let` def wont get the object of asset, and generate an error when init the ledger Signed-off-by: James --- .../chaincode-javascript/lib/asset_transfer_ledger_chaincode.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/asset-transfer-ledger-queries/chaincode-javascript/lib/asset_transfer_ledger_chaincode.js b/asset-transfer-ledger-queries/chaincode-javascript/lib/asset_transfer_ledger_chaincode.js index 83a62049..31174f4e 100644 --- a/asset-transfer-ledger-queries/chaincode-javascript/lib/asset_transfer_ledger_chaincode.js +++ b/asset-transfer-ledger-queries/chaincode-javascript/lib/asset_transfer_ledger_chaincode.js @@ -386,7 +386,7 @@ class Chaincode extends Contract { }, ]; - for (let asset in assets) { + for (const asset of assets) { await this.CreateAsset( ctx, asset.assetID, From 79485c5bab6222f297213934caf1349c23907fe7 Mon Sep 17 00:00:00 2001 From: Julian Castrence Date: Fri, 23 Oct 2020 12:47:55 -0400 Subject: [PATCH 35/38] Implemented Approve, Allowance, and TransferFrom of ERC-20 FAB-18275 Signed-off-by: Julian Castrence --- .../chaincode-go/chaincode/token_contract.go | 215 ++++++++++++++---- 1 file changed, 168 insertions(+), 47 deletions(-) diff --git a/token-account-based/chaincode-go/chaincode/token_contract.go b/token-account-based/chaincode-go/chaincode/token_contract.go index b7b2783b..ad73ead9 100644 --- a/token-account-based/chaincode-go/chaincode/token_contract.go +++ b/token-account-based/chaincode-go/chaincode/token_contract.go @@ -11,6 +11,9 @@ import ( // Define key names for options const totalSupplyKey = "totalSupply" +// Define objectType names for prefix +const allowancePrefix = "allowance" + // SmartContract provides functions for transferring tokens between accounts type SmartContract struct { contractapi.Contract @@ -87,60 +90,17 @@ func (s *SmartContract) Mint(ctx contractapi.TransactionContextInterface, amount // recipient account must be a valid clientID as returned by the ClientID() function. func (s *SmartContract) Transfer(ctx contractapi.TransactionContextInterface, recipient string, amount int) error { - if amount < 0 { // transfer of 0 is allowed in ERC20, so just validate against negative amounts - return fmt.Errorf("transfer amount cannot be negative") - } - // Get ID of submitting client identity clientID, err := ctx.GetClientIdentity().GetID() if err != nil { return fmt.Errorf("failed to get client id: %v", err) } - clientCurrentBalanceBytes, err := ctx.GetStub().GetState(clientID) + err = transferHelper(ctx, clientID, recipient, amount) if err != nil { - return fmt.Errorf("failed to read client account %s from world state: %v", clientID, err) + return fmt.Errorf("failed to transfer: %v", err) } - if clientCurrentBalanceBytes == nil { - return fmt.Errorf("client account %s has no balance", clientID) - } - - clientCurrentBalance, _ := strconv.Atoi(string(clientCurrentBalanceBytes)) // Error handling not needed since Itoa() was used when setting the account balance, guaranteeing it was an integer. - - if clientCurrentBalance < amount { - return fmt.Errorf("client account %s has insufficient funds", clientID) - } - - recipientCurrentBalanceBytes, err := ctx.GetStub().GetState(recipient) - if err != nil { - return fmt.Errorf("failed to read recipient account %s from world state: %v", recipient, err) - } - - var recipientCurrentBalance int - // If recipient current balance doesn't yet exist, we'll create it with a current balance of 0 - if recipientCurrentBalanceBytes == nil { - recipientCurrentBalance = 0 - } else { - recipientCurrentBalance, _ = strconv.Atoi(string(recipientCurrentBalanceBytes)) // Error handling not needed since Itoa() was used when setting the account balance, guaranteeing it was an integer. - } - - clientUpdatedBalance := clientCurrentBalance - amount - recipientUpdatedBalance := recipientCurrentBalance + amount - - err = ctx.GetStub().PutState(clientID, []byte(strconv.Itoa(clientUpdatedBalance))) - if err != nil { - return err - } - - err = ctx.GetStub().PutState(recipient, []byte(strconv.Itoa(recipientUpdatedBalance))) - if err != nil { - return err - } - - log.Printf("client %s balance updated from %d to %d", clientID, clientCurrentBalance, clientUpdatedBalance) - log.Printf("recipient %s balance updated from %d to %d", recipient, recipientCurrentBalance, recipientUpdatedBalance) - return nil } @@ -213,7 +173,168 @@ func (s *SmartContract) TotalSupply(ctx contractapi.TransactionContextInterface) totalSupply, _ = strconv.Atoi(string(totalSupplyBytes)) // Error handling not needed since Itoa() was used when setting the totalSupply, guaranteeing it was an integer. } - log.Printf("The totalSupply was queried: %d total tokens", totalSupply) - return totalSupply, nil + log.Printf("TotalSupply: %d tokens", totalSupply) + return totalSupply, nil +} + +// Approve allows the spender to withdraw from the calling client's token account. +// The spender can withdraw multiple times if necessary, up to the value amount. +func (s *SmartContract) Approve(ctx contractapi.TransactionContextInterface, spender string, value int) error { + + // Get ID of submitting client identity + owner, err := ctx.GetClientIdentity().GetID() + if err != nil { + return fmt.Errorf("failed to get client id: %v", err) + } + + // Create allowanceKey + allowanceKey, err := ctx.GetStub().CreateCompositeKey(allowancePrefix, []string{owner, spender}) + if err != nil { + return fmt.Errorf("failed to create the composite key for prefix %s: %v", allowancePrefix, err) + } + + // Update the state of the smart contract by adding the allowanceKey and value + err = ctx.GetStub().PutState(allowanceKey, []byte(strconv.Itoa(value))) + if err != nil { + return fmt.Errorf("failed to update state of smart contract for key %s: %v", allowanceKey, err) + } + + log.Printf("client %s approved a withdrawal allowance of %d for spender %s", owner, value, spender) + + return nil +} + +// Allowance returns the amount still available for the spender to withdraw from the owner. +func (s *SmartContract) Allowance(ctx contractapi.TransactionContextInterface, owner string, spender string) (int, error) { + + // Create allowanceKey + allowanceKey, err := ctx.GetStub().CreateCompositeKey(allowancePrefix, []string{owner, spender}) + if err != nil { + return 0, fmt.Errorf("failed to create the composite key for prefix %s: %v", allowancePrefix, err) + } + + // Read the allowance amount from the world state + allowanceBytes, err := ctx.GetStub().GetState(allowanceKey) + if err != nil { + return 0, fmt.Errorf("failed to read allowance for %s from world state: %v", allowanceKey, err) + } + + var allowance int + + // If no current allowance, set allowance to 0 + if allowanceBytes == nil { + allowance = 0 + } else { + allowance, err = strconv.Atoi(string(allowanceBytes)) // Error handling not needed since Itoa() was used when setting the totalSupply, guaranteeing it was an integer. + } + + log.Printf("The allowance left for spender %s to withdraw from owner %s: %d", spender, owner, allowance) + + return allowance, nil +} + +// TransferFrom transfers the value amount from the "from" address to the "to" address. +// This function triggers a Transfer event. +func (s *SmartContract) TransferFrom(ctx contractapi.TransactionContextInterface, from string, to string, value int) error { + + // Get ID of submitting client identity + spender, err := ctx.GetClientIdentity().GetID() + if err != nil { + return fmt.Errorf("failed to get client id: %v", err) + } + + // Create allowanceKey + allowanceKey, err := ctx.GetStub().CreateCompositeKey(allowancePrefix, []string{from, spender}) + if err != nil { + return fmt.Errorf("failed to create the composite key for prefix %s: %v", allowancePrefix, err) + } + + // Retrieve the allowance of the spender + currentAllowanceBytes, err := ctx.GetStub().GetState(allowanceKey) + if err != nil { + return fmt.Errorf("failed to retrieve the allowance for %s from world state: %v", allowanceKey, err) + } + + var currentAllowance int + currentAllowance, _ = strconv.Atoi(string(currentAllowanceBytes)) // Error handling not needed since Itoa() was used when setting the totalSupply, guaranteeing it was an integer. + + // Check if transferred value is less than allowance + if currentAllowance < value { + return fmt.Errorf("spender does not have enough allowance for transfer") + } + + // Initiate the transfer + err = transferHelper(ctx, from, to, value) + if err != nil { + return fmt.Errorf("failed to transfer: %v", err) + } + + // Decrease the allowance + updatedAllowance := currentAllowance - value + err = ctx.GetStub().PutState(allowanceKey, []byte(strconv.Itoa(updatedAllowance))) + if err != nil { + return err + } + + log.Printf("spender %s allowance updated from %d to %d", spender, currentAllowance, updatedAllowance) + + return nil +} + +// Helper Functions + +// transferHelper is a helper function that transfers tokens from the "from" address to the "to" address. +// Dependant functions include Transfer and TransferFrom. +func transferHelper(ctx contractapi.TransactionContextInterface, from string, to string, value int) error { + + if value < 0 { // transfer of 0 is allowed in ERC20, so just validate against negative amounts + return fmt.Errorf("transfer amount cannot be negative") + } + + fromCurrentBalanceBytes, err := ctx.GetStub().GetState(from) + if err != nil { + return fmt.Errorf("failed to read client account %s from world state: %v", from, err) + } + + if fromCurrentBalanceBytes == nil { + return fmt.Errorf("client account %s has no balance", from) + } + + fromCurrentBalance, _ := strconv.Atoi(string(fromCurrentBalanceBytes)) // Error handling not needed since Itoa() was used when setting the account balance, guaranteeing it was an integer. + + if fromCurrentBalance < value { + return fmt.Errorf("client account %s has insufficient funds", from) + } + + toCurrentBalanceBytes, err := ctx.GetStub().GetState(to) + if err != nil { + return fmt.Errorf("failed to read recipient account %s from world state: %v", to, err) + } + + var toCurrentBalance int + // If recipient current balance doesn't yet exist, we'll create it with a current balance of 0 + if toCurrentBalanceBytes == nil { + toCurrentBalance = 0 + } else { + toCurrentBalance, _ = strconv.Atoi(string(toCurrentBalanceBytes)) // Error handling not needed since Itoa() was used when setting the account balance, guaranteeing it was an integer. + } + + fromUpdatedBalance := fromCurrentBalance - value + toUpdatedBalance := toCurrentBalance + value + + err = ctx.GetStub().PutState(from, []byte(strconv.Itoa(fromUpdatedBalance))) + if err != nil { + return err + } + + err = ctx.GetStub().PutState(to, []byte(strconv.Itoa(toUpdatedBalance))) + if err != nil { + return err + } + + log.Printf("client %s balance updated from %d to %d", from, fromCurrentBalance, fromUpdatedBalance) + log.Printf("recipient %s balance updated from %d to %d", to, toCurrentBalance, toUpdatedBalance) + + return nil } From d3bc97fae2a6764304c339656fb0eecd7135eeab Mon Sep 17 00:00:00 2001 From: Julian Castrence Date: Thu, 12 Nov 2020 12:44:17 -0500 Subject: [PATCH 36/38] Add Chaincode Events to Transfer and Approve (#369) FAB-18275 Signed-off-by: Julian Castrence Co-authored-by: Julian Castrence --- .../chaincode-go/chaincode/token_contract.go | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/token-account-based/chaincode-go/chaincode/token_contract.go b/token-account-based/chaincode-go/chaincode/token_contract.go index ad73ead9..c01875e1 100644 --- a/token-account-based/chaincode-go/chaincode/token_contract.go +++ b/token-account-based/chaincode-go/chaincode/token_contract.go @@ -1,6 +1,7 @@ package chaincode import ( + "encoding/json" "fmt" "log" "strconv" @@ -101,6 +102,17 @@ func (s *SmartContract) Transfer(ctx contractapi.TransactionContextInterface, re return fmt.Errorf("failed to transfer: %v", err) } + // Emit the Transfer event + transferEvent := map[string]string{"from": clientID, "to": recipient, "value": strconv.Itoa(amount)} + transferEventJSON, err := json.Marshal(transferEvent) + if err != nil { + return fmt.Errorf("failed to obtain JSON encoding: %v", err) + } + err = ctx.GetStub().SetEvent("Transfer", transferEventJSON) + if err != nil { + return fmt.Errorf("event failed to register: %v", err) + } + return nil } @@ -200,6 +212,17 @@ func (s *SmartContract) Approve(ctx contractapi.TransactionContextInterface, spe return fmt.Errorf("failed to update state of smart contract for key %s: %v", allowanceKey, err) } + // Emit the Approval event + approvalEvent := map[string]string{"owner": owner, "spender": spender, "value": strconv.Itoa(value)} + approvalEventJSON, err := json.Marshal(approvalEvent) + if err != nil { + return fmt.Errorf("failed to obtain JSON encoding: %v", err) + } + err = ctx.GetStub().SetEvent("Approval", approvalEventJSON) + if err != nil { + return fmt.Errorf("event failed to register: %v", err) + } + log.Printf("client %s approved a withdrawal allowance of %d for spender %s", owner, value, spender) return nil From 1829666e3a89a0377f6da8b3b13c3b87386fb8ae Mon Sep 17 00:00:00 2001 From: Julian Castrence Date: Mon, 16 Nov 2020 17:51:55 -0500 Subject: [PATCH 37/38] Rename token-account-based Directory to token-erc-20 FAB-18275 Signed-off-by: Julian Castrence --- README.md | 2 +- token-account-based/chaincode-go/go.mod | 5 ----- {token-account-based => token-erc-20}/README.md | 4 ++-- .../chaincode-go/chaincode/token_contract.go | 2 +- token-erc-20/chaincode-go/go.mod | 5 +++++ {token-account-based => token-erc-20}/chaincode-go/go.sum | 0 .../chaincode-go/token_erc_20.go | 6 +++--- .../chaincode-javascript/.editorconfig | 0 .../chaincode-javascript/.eslintignore | 0 .../chaincode-javascript/.eslintrc.js | 0 .../chaincode-javascript/.gitignore | 0 .../chaincode-javascript/index.js | 0 .../chaincode-javascript/lib/tokenERC20.js | 0 .../chaincode-javascript/package.json | 0 .../chaincode-javascript/test/tokenERC20.test.js | 0 15 files changed, 12 insertions(+), 12 deletions(-) delete mode 100644 token-account-based/chaincode-go/go.mod rename {token-account-based => token-erc-20}/README.md (99%) rename {token-account-based => token-erc-20}/chaincode-go/chaincode/token_contract.go (99%) create mode 100644 token-erc-20/chaincode-go/go.mod rename {token-account-based => token-erc-20}/chaincode-go/go.sum (100%) rename token-account-based/chaincode-go/token_account_based.go => token-erc-20/chaincode-go/token_erc_20.go (57%) rename {token-account-based => token-erc-20}/chaincode-javascript/.editorconfig (100%) rename {token-account-based => token-erc-20}/chaincode-javascript/.eslintignore (100%) rename {token-account-based => token-erc-20}/chaincode-javascript/.eslintrc.js (100%) rename {token-account-based => token-erc-20}/chaincode-javascript/.gitignore (100%) rename {token-account-based => token-erc-20}/chaincode-javascript/index.js (100%) rename {token-account-based => token-erc-20}/chaincode-javascript/lib/tokenERC20.js (100%) rename {token-account-based => token-erc-20}/chaincode-javascript/package.json (100%) rename {token-account-based => token-erc-20}/chaincode-javascript/test/tokenERC20.test.js (100%) diff --git a/README.md b/README.md index c761e44f..81b69893 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ Additional samples demonstrate various Fabric use cases and application patterns | -------------|------------------------------|------------------| | [Commercial paper](commercial-paper) | Explore a use case and detailed application development tutorial in which two organizations use a blockchain network to trade commercial paper. | [Commercial paper tutorial](https://hyperledger-fabric.readthedocs.io/en/latest/tutorial/commercial_paper.html) | | [Off chain data](off_chain_data) | Learn how to use the Peer channel-based event services to build an off-chain database for reporting and analytics. | [Peer channel-based event services](https://hyperledger-fabric.readthedocs.io/en/latest/peer_event_services.html) | -| [Token account-based](token-account-based) | Smart contract demonstrating how to create and transfer fungible tokens using an account-based model. | [README](token-account-based/README.md) | +| [Token ERC-20](token-erc-20) | Smart contract demonstrating how to create and transfer fungible tokens using an account-based model. | [README](token-erc-20/README.md) | | [Token UTXO](token-utxo) | Smart contract demonstrating how to create and transfer fungible tokens using a UTXO (unspent transaction output) model. | [README](token-utxo/README.md) | | [High throughput](high-throughput) | Learn how you can design your smart contract to avoid transaction collisions in high volume environments. | [README](high-throughput/README.md) | | [Chaincode](chaincode) | A set of other sample smart contracts, many of which were used in tutorials prior to the asset transfer sample series. | | diff --git a/token-account-based/chaincode-go/go.mod b/token-account-based/chaincode-go/go.mod deleted file mode 100644 index cac66472..00000000 --- a/token-account-based/chaincode-go/go.mod +++ /dev/null @@ -1,5 +0,0 @@ -module github.com/hyperledger/fabric-samples/token-account-based/chaincode-go - -go 1.14 - -require github.com/hyperledger/fabric-contract-api-go v1.1.0 diff --git a/token-account-based/README.md b/token-erc-20/README.md similarity index 99% rename from token-account-based/README.md rename to token-erc-20/README.md index ca260fba..1e6aa420 100644 --- a/token-account-based/README.md +++ b/token-erc-20/README.md @@ -33,12 +33,12 @@ You can use the test network script to deploy the account-based token contract t **For a Go Contract:** ``` -./network.sh deployCC -ccn token_account -ccp ../token-account-based/chaincode-go/ +./network.sh deployCC -ccn token_account -ccp ../token-erc-20/chaincode-go/ ``` **For a JavaScript Contract:** ``` -./network.sh deployCC -ccn token_account -ccp ../token-account-based/chaincode-javascript/ -ccl javascript +./network.sh deployCC -ccn token_account -ccp ../token-erc-20/chaincode-javascript/ -ccl javascript ``` The above command deploys the go chaincode with short name `token_account`. The smart contract will use the default endorsement policy of majority of channel members. diff --git a/token-account-based/chaincode-go/chaincode/token_contract.go b/token-erc-20/chaincode-go/chaincode/token_contract.go similarity index 99% rename from token-account-based/chaincode-go/chaincode/token_contract.go rename to token-erc-20/chaincode-go/chaincode/token_contract.go index c01875e1..b7ce689a 100644 --- a/token-account-based/chaincode-go/chaincode/token_contract.go +++ b/token-erc-20/chaincode-go/chaincode/token_contract.go @@ -311,7 +311,7 @@ func (s *SmartContract) TransferFrom(ctx contractapi.TransactionContextInterface // Dependant functions include Transfer and TransferFrom. func transferHelper(ctx contractapi.TransactionContextInterface, from string, to string, value int) error { - if value < 0 { // transfer of 0 is allowed in ERC20, so just validate against negative amounts + if value < 0 { // transfer of 0 is allowed in ERC-20, so just validate against negative amounts return fmt.Errorf("transfer amount cannot be negative") } diff --git a/token-erc-20/chaincode-go/go.mod b/token-erc-20/chaincode-go/go.mod new file mode 100644 index 00000000..6bee34ad --- /dev/null +++ b/token-erc-20/chaincode-go/go.mod @@ -0,0 +1,5 @@ +module github.com/hyperledger/fabric-samples/token-erc-20/chaincode-go + +go 1.14 + +require github.com/hyperledger/fabric-contract-api-go v1.1.0 diff --git a/token-account-based/chaincode-go/go.sum b/token-erc-20/chaincode-go/go.sum similarity index 100% rename from token-account-based/chaincode-go/go.sum rename to token-erc-20/chaincode-go/go.sum diff --git a/token-account-based/chaincode-go/token_account_based.go b/token-erc-20/chaincode-go/token_erc_20.go similarity index 57% rename from token-account-based/chaincode-go/token_account_based.go rename to token-erc-20/chaincode-go/token_erc_20.go index 0174084d..2bcc51d4 100644 --- a/token-account-based/chaincode-go/token_account_based.go +++ b/token-erc-20/chaincode-go/token_erc_20.go @@ -8,16 +8,16 @@ import ( "log" "github.com/hyperledger/fabric-contract-api-go/contractapi" - "github.com/hyperledger/fabric-samples/token-account-based/chaincode-go/chaincode" + "github.com/hyperledger/fabric-samples/token-erc-20/chaincode-go/chaincode" ) func main() { tokenChaincode, err := contractapi.NewChaincode(&chaincode.SmartContract{}) if err != nil { - log.Panicf("Error creating token-account-based chaincode: %v", err) + log.Panicf("Error creating token-erc-20 chaincode: %v", err) } if err := tokenChaincode.Start(); err != nil { - log.Panicf("Error starting token-account-based chaincode: %v", err) + log.Panicf("Error starting token-erc-20 chaincode: %v", err) } } diff --git a/token-account-based/chaincode-javascript/.editorconfig b/token-erc-20/chaincode-javascript/.editorconfig similarity index 100% rename from token-account-based/chaincode-javascript/.editorconfig rename to token-erc-20/chaincode-javascript/.editorconfig diff --git a/token-account-based/chaincode-javascript/.eslintignore b/token-erc-20/chaincode-javascript/.eslintignore similarity index 100% rename from token-account-based/chaincode-javascript/.eslintignore rename to token-erc-20/chaincode-javascript/.eslintignore diff --git a/token-account-based/chaincode-javascript/.eslintrc.js b/token-erc-20/chaincode-javascript/.eslintrc.js similarity index 100% rename from token-account-based/chaincode-javascript/.eslintrc.js rename to token-erc-20/chaincode-javascript/.eslintrc.js diff --git a/token-account-based/chaincode-javascript/.gitignore b/token-erc-20/chaincode-javascript/.gitignore similarity index 100% rename from token-account-based/chaincode-javascript/.gitignore rename to token-erc-20/chaincode-javascript/.gitignore diff --git a/token-account-based/chaincode-javascript/index.js b/token-erc-20/chaincode-javascript/index.js similarity index 100% rename from token-account-based/chaincode-javascript/index.js rename to token-erc-20/chaincode-javascript/index.js diff --git a/token-account-based/chaincode-javascript/lib/tokenERC20.js b/token-erc-20/chaincode-javascript/lib/tokenERC20.js similarity index 100% rename from token-account-based/chaincode-javascript/lib/tokenERC20.js rename to token-erc-20/chaincode-javascript/lib/tokenERC20.js diff --git a/token-account-based/chaincode-javascript/package.json b/token-erc-20/chaincode-javascript/package.json similarity index 100% rename from token-account-based/chaincode-javascript/package.json rename to token-erc-20/chaincode-javascript/package.json diff --git a/token-account-based/chaincode-javascript/test/tokenERC20.test.js b/token-erc-20/chaincode-javascript/test/tokenERC20.test.js similarity index 100% rename from token-account-based/chaincode-javascript/test/tokenERC20.test.js rename to token-erc-20/chaincode-javascript/test/tokenERC20.test.js From 75f491f2e4aa2f8145d9fc9ef83acd5222f7d08d Mon Sep 17 00:00:00 2001 From: denyeart Date: Sun, 22 Nov 2020 01:54:25 -0500 Subject: [PATCH 38/38] Update ERC-20 readme (#377) Update ERC-20 readme to indicate the new contract name and add TransferFrom tutorial for Go chaincode. Signed-off-by: David Enyeart --- token-erc-20/README.md | 131 ++++++++++++++++++++++++++++------------- 1 file changed, 91 insertions(+), 40 deletions(-) diff --git a/token-erc-20/README.md b/token-erc-20/README.md index 1e6aa420..2655f173 100644 --- a/token-erc-20/README.md +++ b/token-erc-20/README.md @@ -1,6 +1,6 @@ -# Account-based token scenario +# ERC-20 token scenario -The account-based token smart contract demonstrates how to create and transfer fungible tokens using an account-based model. In an account-based model, there is an account for each participant that holds a balance of tokens. +The ERC-20 token smart contract demonstrates how to create and transfer fungible tokens using an account-based model. In an ERC-20 account-based model, there is an account for each participant that holds a balance of tokens. A mint transaction creates tokens in an account, while a transfer transaction debits the caller's account and credits another account. In this sample it is assumed that only one organization (played by Org1) is in a central banker role and can mint new tokens into their account, while any organization can transfer tokens from their account to a recipient's account. @@ -9,12 +9,12 @@ The client ID is simply a base64-encoded concatenation of the issuer and subject In this tutorial, you will mint and transfer tokens as follows: -- A member of Org1 uses the `Mint` function to create new tokens into their account. The `Mint` function reads the certificate information of the client identity that submitted the transaction using the `GetClientIdentity.GetID()` API and credits the account associated with the client ID with the requested number of tokens. +- A member of Org1 uses the `Mint` function to create new tokens into their account. The `Mint` smart contract function reads the certificate information of the client identity that submitted the transaction using the `GetClientIdentity.GetID()` API and credits the account associated with the client ID with the requested number of tokens. - The same minter client will then use the `Transfer` function to transfer the requested number of tokens to the recipient's account. It is assumed that the recipient has provided their account ID to the transfer caller out of band. The recipient can then transfer tokens to other registered users in the same fashion. ## Bring up the test network -You can run the account-based token transfer scenario using the Fabric test network. Open a command terminal and navigate to the test network directory in your local clone of the `fabric-samples`. We will operate from the `test-network` directory for the remainder of the tutorial. +You can run the ERC-20 token transfer scenario using the Fabric test network. Open a command terminal and navigate to the test network directory in your local clone of the `fabric-samples`. We will operate from the `test-network` directory for the remainder of the tutorial. ``` cd fabric-samples/test-network ``` @@ -29,19 +29,19 @@ The -ca flag is used to deploy the network using certificate authorities. This a ## Deploy the smart contract to the channel -You can use the test network script to deploy the account-based token contract to the channel that was just created. Deploy the smart contract to `mychannel` using the following command: +You can use the test network script to deploy the ERC-20 token contract to the channel that was just created. Deploy the smart contract to `mychannel` using the following command: **For a Go Contract:** ``` -./network.sh deployCC -ccn token_account -ccp ../token-erc-20/chaincode-go/ +./network.sh deployCC -ccn token_erc20 -ccp ../token-erc-20/chaincode-go/ ``` **For a JavaScript Contract:** ``` -./network.sh deployCC -ccn token_account -ccp ../token-erc-20/chaincode-javascript/ -ccl javascript +./network.sh deployCC -ccn token_erc20 -ccp ../token-erc-20/chaincode-javascript/ -ccl javascript ``` -The above command deploys the go chaincode with short name `token_account`. The smart contract will use the default endorsement policy of majority of channel members. +The above command deploys the go chaincode with short name `token_erc20`. The smart contract will use the default endorsement policy of majority of channel members. Since the channel has two members, this implies that we'll need to get peer endorsements from 2 out of the 2 channel members. Now you are ready to call the deployed smart contract via peer CLI calls. But let's first create the client identities for our scenario. @@ -50,7 +50,7 @@ Now you are ready to call the deployed smart contract via peer CLI calls. But le The smart contract supports accounts owned by individual client identities from organizations that are members of the channel. In our scenario, the minter of the tokens will be a member of Org1, while the recipient will belong to Org2. To highlight the connection between the `GetClientIdentity().GetID()` API and the information within a user's certificate, we will register two new identities using the Org1 and Org2 Certificate Authorities (CA's), and then use the CA's to generate each identity's certificate and private key. -First, we need to set the following environment variables to use the the Fabric CA client (and subsequent commands). +First, we need to set the following environment variables to use the Fabric CA client (and subsequent commands). ``` export PATH=${PWD}/../bin:${PWD}:$PATH export FABRIC_CFG_PATH=$PWD/../config/ @@ -66,7 +66,7 @@ You can register a new minter client identity using the `fabric-ca-client` tool: fabric-ca-client register --caname ca-org1 --id.name minter --id.secret minterpw --id.type client --tls.certfiles ${PWD}/organizations/fabric-ca/org1/tls-cert.pem ``` -You can now generate the identity certificates and MSP folder by providing the enroll name and secret to the enroll command: +You can now generate the identity certificates and MSP folder by providing the minter's enroll name and secret to the enroll command: ``` fabric-ca-client enroll -u https://minter:minterpw@localhost:7054 --caname ca-org1 -M ${PWD}/organizations/peerOrganizations/org1.example.com/users/minter@org1.example.com/msp --tls.certfiles ${PWD}/organizations/fabric-ca/org1/tls-cert.pem ``` @@ -88,7 +88,7 @@ You can register a recipient client identity using the `fabric-ca-client` tool: fabric-ca-client register --caname ca-org2 --id.name recipient --id.secret recipientpw --id.type client --tls.certfiles ${PWD}/organizations/fabric-ca/org2/tls-cert.pem ``` -We can now enroll to generate the identity certificates and MSP folder: +We can now enroll to generate the recipient's identity certificates and MSP folder: ``` fabric-ca-client enroll -u https://recipient:recipientpw@localhost:8054 --caname ca-org2 -M ${PWD}/organizations/peerOrganizations/org2.example.com/users/recipient@org2.example.com/msp --tls.certfiles ${PWD}/organizations/fabric-ca/org2/tls-cert.pem ``` @@ -115,12 +115,12 @@ The last environment variable above will be utilized within the CLI invoke comma We can then invoke the smart contract to mint 5000 tokens: ``` -peer chaincode invoke $TARGET_TLS_OPTIONS -C mychannel -n token_account -c '{"function":"Mint","Args":["5000"]}' +peer chaincode invoke $TARGET_TLS_OPTIONS -C mychannel -n token_erc20 -c '{"function":"Mint","Args":["5000"]}' ``` The mint function validated that the client is a member of the minter organization, and then credited the minter client's account with 5000 tokens. We can check the minter client's account balance by calling the `ClientAccountBalance` function. ``` -peer chaincode query -C mychannel -n token_account -c '{"function":"ClientAccountBalance","Args":[]}' +peer chaincode query -C mychannel -n token_erc20 -c '{"function":"ClientAccountBalance","Args":[]}' ``` The function queries the balance of the account associated with the minter client ID and returns: @@ -144,7 +144,7 @@ export CORE_PEER_ADDRESS=localhost:9051 Using the Org2 terminal, the Org2 recipient user can retrieve their own account ID: ``` -peer chaincode query -C mychannel -n token_account -c '{"function":"ClientAccountID","Args":[]}' +peer chaincode query -C mychannel -n token_erc20 -c '{"function":"ClientAccountID","Args":[]}' ``` **For a Go Contract:** @@ -177,13 +177,14 @@ Back in the Org1 terminal, request the transfer of 100 tokens to the recipient a **For a Go Contract:** ``` -peer chaincode invoke $TARGET_TLS_OPTIONS -C mychannel -n token_account -c '{"function":"Transfer","Args":[ "eDUwOTo6Q049cmVjaXBpZW50LE9VPWNsaWVudCxPPUh5cGVybGVkZ2VyLFNUPU5vcnRoIENhcm9saW5hLEM9VVM6OkNOPWNhLm9yZzIuZXhhbXBsZS5jb20sTz1vcmcyLmV4YW1wbGUuY29tLEw9SHVyc2xleSxTVD1IYW1wc2hpcmUsQz1VSw==","100"]}' +export RECIPIENT="eDUwOTo6Q049cmVjaXBpZW50LE9VPWNsaWVudCxPPUh5cGVybGVkZ2VyLFNUPU5vcnRoIENhcm9saW5hLEM9VVM6OkNOPWNhLm9yZzIuZXhhbXBsZS5jb20sTz1vcmcyLmV4YW1wbGUuY29tLEw9SHVyc2xleSxTVD1IYW1wc2hpcmUsQz1VSw==" +peer chaincode invoke $TARGET_TLS_OPTIONS -C mychannel -n token_erc20 -c '{"function":"Transfer","Args":[ "'"$RECIPIENT"'","100"]}' ``` **For a JavaScript Contract:** ``` export RECIPIENT="x509::/C=US/ST=North Carolina/O=Hyperledger/OU=client/CN=recipient::/C=UK/ST=Hampshire/L=Hursley/O=org2.example.com/CN=ca.org2.example.com" -peer chaincode invoke $TARGET_TLS_OPTIONS -C mychannel -n token_account -c '{"function":"Transfer","Args":[ "'"$RECIPIENT"'","100"]}' +peer chaincode invoke $TARGET_TLS_OPTIONS -C mychannel -n token_erc20 -c '{"function":"Transfer","Args":[ "'"$RECIPIENT"'","100"]}' ``` The `Transfer` function validates that the account associated with the calling client ID has sufficient funds for the transfer. @@ -191,7 +192,7 @@ It will then debit the caller's account and credit the recipient's account. Note While still in the Org1 terminal, let's request the minter's account balance again: ``` -peer chaincode query -C mychannel -n token_account -c '{"function":"ClientAccountBalance","Args":[]}' +peer chaincode query -C mychannel -n token_erc20 -c '{"function":"ClientAccountBalance","Args":[]}' ``` The function queries the balance of the account associated with the minter client ID and returns: @@ -201,7 +202,7 @@ The function queries the balance of the account associated with the minter clien And then using the Org2 terminal, let's request the recipient's balance: ``` -peer chaincode query -C mychannel -n token_account -c '{"function":"ClientAccountBalance","Args":[]}' +peer chaincode query -C mychannel -n token_erc20 -c '{"function":"ClientAccountBalance","Args":[]}' ``` The function queries the balance of the account associated with the recipient client ID and returns: @@ -211,17 +212,17 @@ The function queries the balance of the account associated with the recipient cl Congratulations, you've transferred 100 tokens! The Org2 recipient can now transfer tokens to other registered users in the same manner. -## Another scenario (for JavaScript contract only) +## 3rd party transfers (TransferFrom) -This sample has another transfer method called `transferFrom`, which allows an approved spender to transfer fungible tokens on behalf of the account owner. The second scenario demonstrates how to approve the spender and transfer fungible tokens. +This sample has another ERC-20 transfer method called `TransferFrom`, which allows an approved 3rd party spender to transfer fungible tokens on behalf of the account owner. This scenario demonstrates how to approve the spender and transfer fungible tokens. -In this tutorial, you will approve the spender and transfer tokens as follows: +In this scenario, you will approve the spender and transfer tokens as follows: - A minter has already created tokens according to the scenario above. -- The same minter client uses the `approve` function to set the allowance of tokens a spender client can transfer on behalf of the minter. It is assumed that the spender has provided their client ID to the `approve` caller out of band. -- The spender client will then use the `transferFrom` function to transfer the requested number of tokens to the recipient's account on behalf of the minter. It is assumed that the recipient has provided their client ID to the `transferFrom` caller out of band. +- The same minter client uses the `Approve` function to set the allowance of tokens a spender client can transfer on behalf of the minter. It is assumed that the spender has provided their client ID to the `Approve` caller out of band. +- The spender client will then use the `TransferFrom` function to transfer the requested number of tokens to the recipient's account on behalf of the minter. It is assumed that the recipient has provided their client ID to the `TransferFrom` caller out of band. -## Register identities +## Register identity for 3rd party spender You have already brought up the network and deployed the smart contract to the channel. We will use the same network and smart contract. @@ -231,7 +232,7 @@ Back in the Org1 terminal, you can register a new spender client identity using fabric-ca-client register --caname ca-org1 --id.name spender --id.secret spenderpw --id.type client --tls.certfiles ${PWD}/organizations/fabric-ca/org1/tls-cert.pem ``` -You can now generate the identity certificates and MSP folder by providing the enroll name and secret to the enroll command: +You can now generate the identity certificates and MSP folder by providing the spender's enroll name and secret to the enroll command: ``` fabric-ca-client enroll -u https://spender:spenderpw@localhost:7054 --caname ca-org1 -M ${PWD}/organizations/peerOrganizations/org1.example.com/users/spender@org1.example.com/msp --tls.certfiles ${PWD}/organizations/fabric-ca/org1/tls-cert.pem ``` @@ -260,9 +261,18 @@ export TARGET_TLS_OPTIONS="-o localhost:7050 --ordererTLSHostnameOverride ordere Now the Org1 spender can retrieve their own client ID: ``` -peer chaincode query -C mychannel -n token_account -c '{"function":"ClientAccountID","Args":[]}' +peer chaincode query -C mychannel -n token_erc20 -c '{"function":"ClientAccountID","Args":[]}' ``` +**For a Go Contract:** + +The function returns of spender's account ID: +``` +eDUwOTo6Q049c3BlbmRlcixPVT1jbGllbnQsTz1IeXBlcmxlZGdlcixTVD1Ob3J0aCBDYXJvbGluYSxDPVVTOjpDTj1jYS5vcmcxLmV4YW1wbGUuY29tLE89b3JnMS5leGFtcGxlLmNvbSxMPUR1cmhhbSxTVD1Ob3J0aCBDYXJvbGluYSxDPVVT +``` + +**For a JavaScript Contract:** + The function returns of spender's client ID. The result shows that the subject and issuer is indeed the recipient user from Org2: ``` @@ -270,18 +280,38 @@ x509::/C=US/ST=North Carolina/O=Hyperledger/OU=client/CN=spender::/C=US/ST=North ``` After the Org1 spender provides their client ID to the minter, the minter can approve a spender. -Back in the Org1 terminal, request the approval of 100 tokens to be withdrew by the spender.: +Back in the Org1 minter terminal, request the approval of 500 tokens to be withdrawn by the spender. + +**For a Go Contract:** + +``` +export SPENDER="eDUwOTo6Q049c3BlbmRlcixPVT1jbGllbnQsTz1IeXBlcmxlZGdlcixTVD1Ob3J0aCBDYXJvbGluYSxDPVVTOjpDTj1jYS5vcmcxLmV4YW1wbGUuY29tLE89b3JnMS5leGFtcGxlLmNvbSxMPUR1cmhhbSxTVD1Ob3J0aCBDYXJvbGluYSxDPVVT" +peer chaincode invoke $TARGET_TLS_OPTIONS -C mychannel -n token_erc20 -c '{"function":"Approve","Args":[ "'"$SPENDER"'","500"]}' +``` + +**For a JavaScript Contract:** + ``` export SPENDER="x509::/C=US/ST=North Carolina/O=Hyperledger/OU=client/CN=spender::/C=US/ST=North Carolina/L=Durham/O=org1.example.com/CN=ca.org1.example.com" -peer chaincode invoke $TARGET_TLS_OPTIONS -C mychannel -n token_account -c '{"function":"Approve","Args":["'"$SPENDER"'", "500"]}' +peer chaincode invoke $TARGET_TLS_OPTIONS -C mychannel -n token_erc20 -c '{"function":"Approve","Args":["'"$SPENDER"'", "500"]}' ``` -The approve function added that the spender client can consume 500 tokens on behalf of the minter. We can check the spender client's allowance from the minter by calling the `allowance` function. +The approve function specified that the spender client can transfer 500 tokens on behalf of the minter. We can check the spender client's allowance from the minter by calling the `allowance` function. + +Let's request the spender's allowance from the Org1 minter terminal. + +**For a Go Contract:** + +``` +export MINTER="eDUwOTo6Q049bWludGVyLE9VPWNsaWVudCxPPUh5cGVybGVkZ2VyLFNUPU5vcnRoIENhcm9saW5hLEM9VVM6OkNOPWNhLm9yZzEuZXhhbXBsZS5jb20sTz1vcmcxLmV4YW1wbGUuY29tLEw9RHVyaGFtLFNUPU5vcnRoIENhcm9saW5hLEM9VVM=" +peer chaincode query -C mychannel -n token_erc20 -c '{"function":"Allowance","Args":["'"$MINTER"'", "'"$SPENDER"'"]}' +``` + +**For a JavaScript Contract:** -Let's request the spender's allowance from the minter: ``` export MINTER="x509::/C=US/ST=North Carolina/O=Hyperledger/OU=client/CN=minter::/C=US/ST=North Carolina/L=Durham/O=org1.example.com/CN=ca.org1.example.com" -peer chaincode query -C mychannel -n token_account -c '{"function":"Allowance","Args":["'"$MINTER"'", "'"$SPENDER"'"]}' +peer chaincode query -C mychannel -n token_erc20 -c '{"function":"Allowance","Args":["'"$MINTER"'", "'"$SPENDER"'"]}' ``` The function queries the allowance associated with the spender client ID and returns: @@ -293,19 +323,30 @@ The function queries the allowance associated with the spender client ID and ret The spender intends to transfer 100 tokens to the Org2 recipient on behalf of the minter. The spender has already got the minter client Id and the recipient client ID. -Back in the 3rd terminal, request the transfer of 100 tokens to the recipient account: +Back in the 3rd terminal, request the transfer of 100 tokens to the recipient account. + +**For a Go Contract:** + +``` +export MINTER="eDUwOTo6Q049bWludGVyLE9VPWNsaWVudCxPPUh5cGVybGVkZ2VyLFNUPU5vcnRoIENhcm9saW5hLEM9VVM6OkNOPWNhLm9yZzEuZXhhbXBsZS5jb20sTz1vcmcxLmV4YW1wbGUuY29tLEw9RHVyaGFtLFNUPU5vcnRoIENhcm9saW5hLEM9VVM=" +export RECIPIENT="eDUwOTo6Q049cmVjaXBpZW50LE9VPWNsaWVudCxPPUh5cGVybGVkZ2VyLFNUPU5vcnRoIENhcm9saW5hLEM9VVM6OkNOPWNhLm9yZzIuZXhhbXBsZS5jb20sTz1vcmcyLmV4YW1wbGUuY29tLEw9SHVyc2xleSxTVD1IYW1wc2hpcmUsQz1VSw==" +peer chaincode invoke $TARGET_TLS_OPTIONS -C mychannel -n token_erc20 -c '{"function":"TransferFrom","Args":[ "'"$MINTER"'", "'"$RECIPIENT"'", "100"]}' +``` + +**For a JavaScript Contract:** + ``` export MINTER="x509::/C=US/ST=North Carolina/O=Hyperledger/OU=client/CN=minter::/C=US/ST=North Carolina/L=Durham/O=org1.example.com/CN=ca.org1.example.com" export RECIPIENT="x509::/C=US/ST=North Carolina/O=Hyperledger/OU=client/CN=recipient::/C=UK/ST=Hampshire/L=Hursley/O=org2.example.com/CN=ca.org2.example.com" -peer chaincode invoke $TARGET_TLS_OPTIONS -C mychannel -n token_account -c '{"function":"TransferFrom","Args":[ "'"$MINTER"'", "'"$RECIPIENT"'", "100"]}' +peer chaincode invoke $TARGET_TLS_OPTIONS -C mychannel -n token_erc20 -c '{"function":"TransferFrom","Args":[ "'"$MINTER"'", "'"$RECIPIENT"'", "100"]}' ``` The `TransferFrom` function has three args: sender, recipient, amount. The function validates that the account associated with the sender has sufficient funds for the transfer. The function also validates if the allowance associated with the calling client ID exceeds funds to be transferred. It will then debit the sender's account and credit the recipient's account. It will also decrease the spender's allowance approved by the minter. Note that the sample contract will automatically create an account with zero balance for the recipient, if one does not yet exist. -While still in the 3rd terminal, let's request the minter's account balance again: +While still in the 3rd terminal for the spender, let's request the minter's account balance again: ``` -peer chaincode query -C mychannel -n token_account -c '{"function":"BalanceOf","Args":["'"$MINTER"'"]}' +peer chaincode query -C mychannel -n token_erc20 -c '{"function":"BalanceOf","Args":["'"$MINTER"'"]}' ``` The function queries the balance of the account associated with the minter client ID and returns: @@ -313,10 +354,20 @@ The function queries the balance of the account associated with the minter clien 4800 ``` -While still in the 3rd terminal, let's request the spender's allowance from the minter again: +While still in the 3rd terminal for the spender, let's request the spender's allowance from the minter again. + +**For a Go Contract:** + +``` +export SPENDER="eDUwOTo6Q049c3BlbmRlcixPVT1jbGllbnQsTz1IeXBlcmxlZGdlcixTVD1Ob3J0aCBDYXJvbGluYSxDPVVTOjpDTj1jYS5vcmcxLmV4YW1wbGUuY29tLE89b3JnMS5leGFtcGxlLmNvbSxMPUR1cmhhbSxTVD1Ob3J0aCBDYXJvbGluYSxDPVVT" +peer chaincode query -C mychannel -n token_erc20 -c '{"function":"Allowance","Args":["'"$MINTER"'", "'"$SPENDER"'"]}' +``` + +**For a JavaScript Contract:** + ``` export SPENDER="x509::/C=US/ST=North Carolina/O=Hyperledger/OU=client/CN=spender::/C=US/ST=North Carolina/L=Durham/O=org1.example.com/CN=ca.org1.example.com" -peer chaincode query -C mychannel -n token_account -c '{"function":"Allowance","Args":["'"$MINTER"'", "'"$SPENDER"'"]}' +peer chaincode query -C mychannel -n token_erc20 -c '{"function":"Allowance","Args":["'"$MINTER"'", "'"$SPENDER"'"]}' ``` The function queries the allowance associated with the spender client ID and returns: @@ -326,7 +377,7 @@ The function queries the allowance associated with the spender client ID and ret And then using the Org2 terminal, let's request the recipient's balance: ``` -peer chaincode query -C mychannel -n token_account -c '{"function":"ClientAccountBalance","Args":[]}' +peer chaincode query -C mychannel -n token_erc20 -c '{"function":"ClientAccountBalance","Args":[]}' ``` The function queries the balance of the account associated with the recipient client ID and returns: @@ -345,8 +396,8 @@ When you are finished, you can bring down the test network. The command will rem ## Contract extension ideas -You can extend the basic account-based token sample to meet other requirements. For example: +You can extend the basic ERC-20 account-based token sample to meet other requirements. For example: * Rather than using the default 'majority' endorsement policy, you could set the endorsement policy to a subset of organizations that represent trust anchors for the contract execution. -* You could also require that accounts get setup before use, and apply state-based endorsement for each account key that has been created. For example on an OrgA account, set state-based endorsement policy to be OrgA and the central banker (or some other trust anchor). And on an OrgB account, set state-based endorsement policy to be OrgB and the central banker (or some other trust anchor). Then to transfer tokens from an OrgA account to an OrgB account, you would require endorsements from OrgA, OrgB, and the central banker (or some other trust anchor). +* You could also require that accounts get setup before use, and apply state-based endorsement for each account key that has been created. For example on an Org1 account, set state-based endorsement policy to be Org1 and the central banker (or some other trust anchor). And on an Org2 account, set state-based endorsement policy to be Org2 and the central banker (or some other trust anchor). Then to transfer tokens from an Org1 account to an Org2 account, you would require endorsements from Org1, Org2, and the central banker (or some other trust anchor). * You could utilize anonymous addresses for accounts based on private-public key pairs, instead of accounts keyed by the client ID. In order to spend the tokens, the client would have to sign the transfer input as proof that they own the address private key, which the contract would then validate, similar to the Ethereum model in the permissionless blockchain space. However, in a permissioned blockchain such as Fabric, only registered clients are authorized to participate. Furthermore, if you don't want to leak the registered client identity associated with each account, the clients could be registered using an Identity Mixer MSP, so that the client itself is also anonymous in each of the token transactions.